package de.sebse.fuplanner.services.canteen;

import android.content.Context;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

import de.sebse.fuplanner.services.canteen.types.Canteen;
import de.sebse.fuplanner.services.canteen.types.CanteenListener;
import de.sebse.fuplanner.services.canteen.types.Canteens;
import de.sebse.fuplanner.services.canteen.types.Day;
import de.sebse.fuplanner.tools.AsyncQueue;
import de.sebse.fuplanner.tools.network.HTTPService;
import de.sebse.fuplanner.tools.network.NetworkCallback;
import de.sebse.fuplanner.tools.network.NetworkError;
import de.sebse.fuplanner.tools.network.NetworkErrorCallback;

public class CanteenBrowser extends HTTPService {
    private Canteens canteens;
    private final AsyncQueue queue = new AsyncQueue();
    private final Context context;
    private CanteenListener mListener;

    public CanteenBrowser(Context context) {
        super(context);
        this.context = context;
        if (context instanceof CanteenListener)
            mListener = (CanteenListener) context;
        else
            throw new RuntimeException(context.toString() + " must implement CanteenListener");
        try {
            this.canteens = Canteens.load(context);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void getCanteens(final NetworkCallback<Canteens> callback, final NetworkErrorCallback errorCallback) {
        getCanteens(callback, errorCallback, false);
    }

    public void getCanteens(final NetworkCallback<Canteens> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        queue.add("list", () -> {
            if (this.canteens != null && !forceRefresh) {
                callback.onResponse(this.canteens);
                queue.next("list");
                return;
            }
            this.upgradeCanteens(success -> {
                if (this.canteens == null)
                    this.canteens = success;
                else
                    this.canteens.update(success);
                this.save();
                saveOnCallback(callback, forceRefresh).onResponse(this.canteens);
                queue.next("list");
            }, queue.check("list", errorOnCallback(errorCallback)));
        });
    }

    private void upgradeCanteens(final NetworkCallback<Canteens> callback, final NetworkErrorCallback errorCallback) {
        get("https://openmensa.org/api/v2/canteens?near[lat]=52.449743&near[lng]=13.282245&near[dist]=7", null, response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(201101, 403, "No canteen list retrieved!"));
                return;
            }
            Canteens canteens = new Canteens();
            try {
                JSONArray json = new JSONArray(body);

                for (int i = 0; i < json.length(); i++) {
                    JSONObject canteen = json.getJSONObject(i);
                    int id = canteen.getInt("id");
                    String name = canteen.getString("name");
                    String city = canteen.getString("city");
                    String address = canteen.getString("address");
                    JSONArray coords = canteen.getJSONArray("coordinates");
                    double lat = 0;
                    double lng = 0;
                    if (coords != null) {
                        lat = coords.getDouble(0);
                        lng = coords.getDouble(1);
                    }
                    for (int cant : Canteens.availableCanteens) {
                        if (cant == id) {
                            canteens.addCanteen(id, name, city, address, lat, lng);
                            break;
                        }
                    }
                }
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(201102, 403, "Cannot parse canteen list!"));
                return;
            }
            callback.onResponse(canteens);
        }, error -> errorCallback.onError(new NetworkError(201103, error.networkResponse.statusCode, "Cannot get canteen list!")));
    }

    public void getCanteen(Canteen canteen, final NetworkCallback<Canteen> callback, final NetworkErrorCallback errorCallback) {
        getCanteen(canteen, callback, errorCallback, false);
    }

    public void getCanteen(Canteen canteen, final NetworkCallback<Canteen> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        String hash = "canteen" + canteen.getId();
        queue.add(hash, () -> {
            if (canteen.size() > 0 && !forceRefresh) {
                callback.onResponse(canteen);
                queue.next(hash);
                return;
            }
            this.upgradeCanteen(canteen, success -> {
                canteen.update(success);
                this.save();
                saveOnCallback(callback, forceRefresh).onResponse(canteen);
                queue.next(hash);
            }, queue.check(hash, errorOnCallback(errorCallback)));
        });
    }

    private void upgradeCanteen(Canteen canteen, final NetworkCallback<Canteen> callback, final NetworkErrorCallback errorCallback) {
        get(String.format("https://openmensa.org/api/v2/canteens/%s/days", canteen.getId()), null, response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(201201, 403, "No day list retrieved!"));
                return;
            }
            try {
                JSONArray json = new JSONArray(body);

                for (int i = 0; i < json.length(); i++) {
                    JSONObject day = json.getJSONObject(i);
                    String date = day.getString("date");
                    boolean closed = day.getBoolean("closed");

                    canteen.addDay(Canteen.keyToCalendar(date), closed);
                }
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(201202, 403, "Cannot parse day list!"));
                return;
            }
            callback.onResponse(canteen);
        }, error -> errorCallback.onError(new NetworkError(201203, error.networkResponse.statusCode, "Cannot get day list!")));
    }

    public void getDay(Day day, final NetworkCallback<Day> callback, final NetworkErrorCallback errorCallback) {
        getDay(day, callback, errorCallback, false);
    }

    public void getDay(Day day, final NetworkCallback<Day> callback, final NetworkErrorCallback errorCallback, boolean forceRefresh) {
        String hash = "day" + day.getCanteenId() + "@@@" + Canteen.calendarToKey(day.getCalendar());
        queue.add(hash, () -> {
            if (day.size() > 0 && !forceRefresh) {
                callback.onResponse(day);
                queue.next(hash);
                return;
            }
            this.upgradeDay(day, success -> {
                day.update(success);
                this.save();
                saveOnCallback(callback, forceRefresh).onResponse(day);
                queue.next(hash);
            }, queue.check(hash, errorOnCallback(errorCallback)));
        });
    }

    private void upgradeDay(Day day, final NetworkCallback<Day> callback, final NetworkErrorCallback errorCallback) {
        get(String.format("https://openmensa.org/api/v2/canteens/%s/days/%s/meals/", day.getCanteenId(), Canteen.calendarToKey(day.getCalendar())), null, response -> {
            String body = response.getParsed();
            if (body == null) {
                errorCallback.onError(new NetworkError(201301, 403, "No meal list retrieved!"));
                return;
            }
            try {
                JSONArray json = new JSONArray(body);

                for (int i = 0; i < json.length(); i++) {
                    JSONObject meal = json.getJSONObject(i);
                    int id = meal.getInt("id");
                    String name = meal.getString("name");
                    String category = meal.getString("category");
                    JSONObject prices = meal.getJSONObject("prices");
                    double priceStdnt = 0;
                    double priceEmply = 0;
                    double priceOther = 0;
                    if (prices != null) {
                        priceOther = prices.optDouble("others", -1);
                        priceEmply = prices.optDouble("employees", priceOther);
                        priceStdnt = prices.optDouble("students", priceEmply);
                    }
                    JSONArray noteArray = meal.getJSONArray("notes");
                    String[] notes = new String[noteArray.length()];
                    for (int j = 0; j < noteArray.length(); j++) {
                        notes[j] = noteArray.getString(j);
                    }

                    day.addMeal(id, name, category, priceStdnt, priceEmply, priceOther, notes);
                }
            } catch (JSONException e) {
                e.printStackTrace();
                errorCallback.onError(new NetworkError(201302, 403, "Cannot parse meal list!"));
                return;
            }
            callback.onResponse(day);
        }, error -> errorCallback.onError(new NetworkError(201303, error.networkResponse.statusCode, "Cannot get meal list!")));
    }

    private void save() {
        try {
            this.canteens.save(this.context);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private<T> NetworkCallback<T> saveOnCallback(NetworkCallback<T> callback, boolean forceRefresh){
        return (success -> {
            if (forceRefresh)
                mListener.onCanteenRefreshCompleted(false);
            callback.onResponse(success);
        });
    }

    private NetworkErrorCallback errorOnCallback(NetworkErrorCallback errorCallback){
        return (error -> {
            mListener.onCanteenRefreshCompleted(true);
            errorCallback.onError(error);
        });
    }
}
