/*
 * Copyright 2013 serso aka se.solovyev
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * Contact details
 *
 * Email: se.solovyev@gmail.com
 * Site:  http://se.solovyev.org
 */

package org.solovyev.android.calculator.functions;

import static android.text.TextUtils.isEmpty;

import android.support.annotation.NonNull;

import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import org.solovyev.android.Check;
import org.solovyev.android.calculator.R;
import org.solovyev.android.calculator.entities.BaseEntitiesRegistry;
import org.solovyev.android.calculator.entities.Category;
import org.solovyev.android.calculator.entities.Entities;
import org.solovyev.android.calculator.json.Json;
import org.solovyev.android.calculator.json.Jsonable;
import org.solovyev.android.io.FileSaver;
import org.solovyev.common.text.Strings;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import jscl.JsclMathEngine;
import jscl.math.function.CustomFunction;
import jscl.math.function.Function;
import jscl.math.function.IFunction;

@Singleton
public class FunctionsRegistry extends BaseEntitiesRegistry<Function> {

    {
        addDescription("sin", R.string.c_fun_description_sin);
        addDescription("cos", R.string.c_fun_description_cos);
        addDescription("tan", R.string.c_fun_description_tan);
        addDescription("cot", R.string.c_fun_description_cot);
        addDescription("asin", R.string.c_fun_description_asin);
        addDescription("acos", R.string.c_fun_description_acos);
        addDescription("atan", R.string.c_fun_description_atan);
        addDescription("acot", R.string.c_fun_description_acot);
        addDescription("ln", R.string.c_fun_description_ln);
        addDescription("lg", R.string.c_fun_description_lg);
        addDescription("log", R.string.c_fun_description_log);
        addDescription("exp", R.string.c_fun_description_exp);
        addDescription("√", R.string.c_fun_description_sqrt);
        addDescription("sqrt", R.string.c_fun_description_sqrt);
        addDescription("cubic", R.string.c_fun_description_cubic);
        addDescription("abs", R.string.c_fun_description_abs);
        addDescription("sgn", R.string.c_fun_description_sgn);
        addDescription("eq", R.string.c_fun_description_eq);
        addDescription("le", R.string.c_fun_description_le);
        addDescription("ge", R.string.c_fun_description_ge);
        addDescription("ne", R.string.c_fun_description_ne);
        addDescription("lt", R.string.c_fun_description_lt);
        addDescription("gt", R.string.c_fun_description_gt);
        addDescription("rad", R.string.c_fun_description_rad);
        addDescription("dms", R.string.c_fun_description_dms);
        addDescription("deg", R.string.c_fun_description_deg);
    }

    @Inject
    public FunctionsRegistry(@Nonnull JsclMathEngine mathEngine) {
        super(mathEngine.getFunctionsRegistry());
    }

    public void addOrUpdate(@Nonnull Function newFunction, @Nullable Function oldFunction) {
        final Function function = addOrUpdate(newFunction);
        if (oldFunction == null) {
            bus.post(new AddedEvent(function));
        } else {
            bus.post(new ChangedEvent(oldFunction, function));
        }
    }

    @Override
    protected void onInit() {
        Check.isNotMainThread();
        migrateOldFunctions();

        final List<CustomFunction.Builder> functions = new ArrayList<>();
        functions.add(new CustomFunction.Builder(true, "log", Arrays.asList("base", "x"),
                "ln(x)/ln(base)"));
        functions.add(new CustomFunction.Builder(true, "√3", Collections.singletonList("x"),
                "x^(1/3)"));
        functions.add(new CustomFunction.Builder(true, "√4", Collections.singletonList("x"),
                "x^(1/4)"));
        functions.add(new CustomFunction.Builder(true, "√n", Arrays.asList("x", "n"), "x^(1/n)"));
        functions.add(new CustomFunction.Builder(true, "re", Collections.singletonList("x"),
                "(x+conjugate(x))/2"));
        functions.add(new CustomFunction.Builder(true, "im", Collections.singletonList("x"),
                "(x-conjugate(x))/(2*i)"));

        for (CppFunction function : loadEntities(CppFunction.JSON_CREATOR)) {
            functions.add(function.toJsclBuilder());
        }
        addSafely(functions);
    }

    /**
     * As some functions might depend on not-yet-loaded functions we need to try to add all functions first and then
     * re-run again if there are functions left. This process should continue until we can't add more functions
     * @param functions functions to add
     */
    private void addSafely(@Nonnull List<CustomFunction.Builder> functions) {
        final List<Exception> exceptions = new ArrayList<>();
        while (functions.size() > 0) {
            final int sizeBefore = functions.size();
            // prepare exceptions list for new round
            exceptions.clear();
            addSafely(functions, exceptions);
            final int sizeAfter = functions.size();
            if (sizeBefore == sizeAfter) {
                break;
            }
        }

        if (functions.size() > 0) {
            // report exceptions
            for (Exception exception : exceptions) {
                errorReporter.onException(exception);
            }
        }
    }

    private void addSafely(@Nonnull List<CustomFunction.Builder> functions, @Nonnull List<Exception> exceptions) {
        for (Iterator<CustomFunction.Builder> iterator = functions.iterator(); iterator.hasNext(); ) {
            final CustomFunction.Builder function = iterator.next();
            try {
                addSafely(function.create());
                iterator.remove();
            } catch (Exception e) {
                exceptions.add(e);
            }
        }
    }

    @Override
    public void remove(@Nonnull Function function) {
        super.remove(function);
        bus.post(new RemovedEvent(function));
    }

    @Nullable
    @Override
    protected Jsonable toJsonable(@NonNull Function function) {
        if (function instanceof IFunction) {
            return CppFunction.builder((IFunction) function).build();
        }
        return null;
    }

    private void migrateOldFunctions() {
        final String xml = preferences.getString(OldFunctions.PREFS_KEY, null);
        if (isEmpty(xml)) {
            return;
        }
        try {
            final Serializer serializer = new Persister();
            final OldFunctions oldFunctions = serializer.read(OldFunctions.class, xml);
            if (oldFunctions != null) {
                List<CppFunction> functions = OldFunctions.toCppFunctions(oldFunctions);
                FileSaver.save(getEntitiesFile(), Json.toJson(functions).toString());
            }
            preferences.edit().remove(OldFunctions.PREFS_KEY).apply();
        } catch (Exception e) {
            errorReporter.onException(e);
        }
    }

    @Override
    @NonNull
    protected File getEntitiesFile() {
        return new File(filesDir.get(), "functions.json");
    }

    @Override
    public Category getCategory(@Nonnull Function function) {
        return Entities.getCategory(function, FunctionCategory.values());
    }

    @Nullable
    @Override
    public String getDescription(@Nonnull String name) {
        final Function function = get(name);

        String description = null;
        if (function instanceof CustomFunction) {
            description = ((CustomFunction) function).getDescription();
        }

        if (!Strings.isEmpty(description)) {
            return description;
        }
        return super.getDescription(name);

    }

    public static final class RemovedEvent {
        @NonNull
        public final Function function;

        public RemovedEvent(@NonNull Function function) {
            this.function = function;
        }
    }

    public static final class AddedEvent {
        @NonNull
        public final Function function;

        public AddedEvent(@NonNull Function function) {
            this.function = function;
        }
    }

    public static final class ChangedEvent {
        @NonNull
        public final Function oldFunction;
        @NonNull
        public final Function newFunction;

        public ChangedEvent(@NonNull Function oldFunction, @NonNull Function newFunction) {
            this.oldFunction = oldFunction;
            this.newFunction = newFunction;
        }
    }
}
