/*
 * 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;

import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.text.TextUtils;

import com.squareup.otto.Bus;

import org.solovyev.android.Check;
import org.solovyev.android.calculator.functions.FunctionsRegistry;
import org.solovyev.android.calculator.math.MathType;
import org.solovyev.android.calculator.operators.OperatorsRegistry;
import org.solovyev.android.calculator.operators.PostfixFunctionsRegistry;
import org.solovyev.android.calculator.preferences.PreferenceEntry;
import org.solovyev.android.prefs.IntegerPreference;
import org.solovyev.android.prefs.Preference;
import org.solovyev.android.prefs.StringPreference;
import org.solovyev.common.NumberFormatter;
import org.solovyev.common.text.CharacterMapper;
import org.solovyev.common.text.EnumMapper;
import org.solovyev.common.text.NumberMapper;

import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;

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

import jscl.AngleUnit;
import jscl.JsclMathEngine;
import jscl.MathEngine;
import jscl.NumeralBase;
import jscl.math.operator.Operator;
import jscl.text.Identifier;
import jscl.text.Parser;
import midpcalc.Real;

@Singleton
public class Engine implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Nonnull
    private final MathEngine mathEngine;
    @Inject
    SharedPreferences preferences;
    @Inject
    Bus bus;
    @Inject
    ErrorReporter errorReporter;
    @Inject
    FunctionsRegistry functionsRegistry;
    @Inject
    VariablesRegistry variablesRegistry;
    @Inject
    OperatorsRegistry operatorsRegistry;
    @Inject
    PostfixFunctionsRegistry postfixFunctionsRegistry;
    @Nonnull
    private String multiplicationSign = Preferences.multiplicationSign.getDefaultValue();

    public Engine(@Nonnull MathEngine mathEngine, @Nonnull VariablesRegistry variablesRegistry, @Nonnull FunctionsRegistry functionsRegistry, @Nonnull OperatorsRegistry operatorsRegistry, @Nonnull PostfixFunctionsRegistry postfixFunctionsRegistry) {
        this.mathEngine = mathEngine;
        this.variablesRegistry = variablesRegistry;
        this.functionsRegistry = functionsRegistry;
        this.operatorsRegistry = operatorsRegistry;
        this.postfixFunctionsRegistry = postfixFunctionsRegistry;
    }

    @Inject
    public Engine(@Nonnull JsclMathEngine mathEngine) {
        this.mathEngine = mathEngine;

        this.mathEngine.setPrecision(5);
        this.mathEngine.setGroupingSeparator(JsclMathEngine.GROUPING_SEPARATOR_DEFAULT);
    }

    private static void migratePreference(@Nonnull SharedPreferences preferences, @Nonnull StringPreference<?> preference, @Nonnull String oldKey, @Nonnull SharedPreferences.Editor editor) {
        if (!preferences.contains(oldKey)) {
            return;
        }
        editor.putString(preference.getKey(), preferences.getString(oldKey, null));
    }

    public static boolean isValidName(@Nullable String name) {
        if (!TextUtils.isEmpty(name)) {
            try {
                final String parsed = Identifier.parser.parse(Parser.Parameters.get(name), null);
                return TextUtils.equals(parsed, name);
            } catch (jscl.text.ParseException e) {
                // not valid name;
            }
        }

        return false;
    }

    @Nonnull
    public VariablesRegistry getVariablesRegistry() {
        return variablesRegistry;
    }

    @Nonnull
    public FunctionsRegistry getFunctionsRegistry() {
        return functionsRegistry;
    }

    @Nonnull
    public EntitiesRegistry<Operator> getOperatorsRegistry() {
        return operatorsRegistry;
    }

    @Nonnull
    public EntitiesRegistry<Operator> getPostfixFunctionsRegistry() {
        return postfixFunctionsRegistry;
    }

    @Nonnull
    public MathEngine getMathEngine() {
        return mathEngine;
    }

    public void init(@Nonnull Executor initThread) {
        Check.isMainThread();
        checkPreferences();
        preferences.registerOnSharedPreferenceChangeListener(this);
        applyPreferences();
        initThread.execute(new Runnable() {
            @Override
            public void run() {
                initAsync();
            }
        });
    }

    private void initPreferences(SharedPreferences.Editor editor) {
        if (!Engine.Preferences.Output.separator.isSet(preferences)) {
            final Locale locale = Locale.getDefault();
            if (locale != null) {
                final DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(locale);
                final int index = MathType.grouping_separator.getTokens().indexOf(String.valueOf(decimalFormatSymbols.getGroupingSeparator()));
                final char separator;
                if (index >= 0) {
                    separator = MathType.grouping_separator.getTokens().get(index).charAt(0);
                } else {
                    separator = JsclMathEngine.GROUPING_SEPARATOR_DEFAULT;
                }

                Engine.Preferences.Output.separator.putPreference(editor, separator);
            }
        }

        Engine.Preferences.angleUnit.tryPutDefault(preferences, editor);
        Engine.Preferences.numeralBase.tryPutDefault(preferences, editor);
        Preferences.Output.notation.tryPutDefault(preferences, editor);
        Preferences.Output.separator.tryPutDefault(preferences, editor);
        Preferences.Output.precision.tryPutDefault(preferences, editor);

    }

    private void checkPreferences() {
        final int oldVersion;
        if (Preferences.version.isSet(preferences)) {
            oldVersion = Preferences.version.getPreference(preferences);
        } else {
            oldVersion = 0;
        }
        final int newVersion = Preferences.version.getDefaultValue();
        if (oldVersion == newVersion) {
            return;
        }
        final SharedPreferences.Editor editor = preferences.edit();
        if (oldVersion == 0) {
            migratePreference(preferences, Preferences.Output.separator, "org.solovyev.android.calculator.CalculatorActivity_calc_grouping_separator", editor);
            migratePreference(preferences, Preferences.multiplicationSign, "org.solovyev.android.calculator.CalculatorActivity_calc_multiplication_sign", editor);
            migratePreference(preferences, Preferences.numeralBase, "org.solovyev.android.calculator.CalculatorActivity_numeral_bases", editor);
            migratePreference(preferences, Preferences.angleUnit, "org.solovyev.android.calculator.CalculatorActivity_angle_units", editor);
            migratePreference(preferences, Preferences.Output.precision, "org.solovyev.android.calculator.CalculatorModel_result_precision", editor);
            if (preferences.contains("engine.output.science_notation")) {
                final boolean scientific = preferences.getBoolean("engine.output.science_notation", false);
                Preferences.Output.notation.putPreference(editor, scientific ? Notation.sci : Notation.dec);
            }
            if (preferences.contains("org.solovyev.android.calculator.CalculatorModel_round_result")) {
                final boolean round = preferences.getBoolean("org.solovyev.android.calculator.CalculatorModel_round_result", true);
                if (!round) {
                    Preferences.Output.precision.putPreference(editor, NumberFormatter.MAX_PRECISION);
                }
            }
            // #initPreferences rely on all changes to be committed
            editor.apply();
            initPreferences(editor);
        } else if (oldVersion == 1) {
            migratePreference(preferences, Preferences.Output.separator, "engine.groupingSeparator", editor);
            if (preferences.contains("engine.output.scientificNotation")) {
                final boolean scientific = preferences.getBoolean("engine.output.scientificNotation", false);
                Preferences.Output.notation.putPreference(editor, scientific ? Notation.sci : Notation.dec);
            }
            if (preferences.contains("engine.output.round")) {
                final boolean round = preferences.getBoolean("engine.output.round", true);
                if (!round) {
                    Preferences.Output.precision.putPreference(editor, NumberFormatter.MAX_PRECISION);
                }
            }
            // #initPreferences rely on all changes to be committed
            editor.apply();
            // preferences should be initialized again as:
            // 1. It was forgotten for 0 version
            // 2. There is a bunch of new preferences
            initPreferences(editor);
        }
        Preferences.version.putDefault(editor);
        editor.apply();
    }

    private void initAsync() {
        init(variablesRegistry);
        init(functionsRegistry);
        init(operatorsRegistry);
        init(postfixFunctionsRegistry);
    }

    private void init(@Nonnull EntitiesRegistry<?> registry) {
        try {
            registry.init();
        } catch (Exception e) {
            errorReporter.onException(e);
        }
    }

    private void applyPreferences() {
        Check.isMainThread();
        mathEngine.setAngleUnits(Preferences.angleUnit.getPreference(preferences));
        mathEngine.setNumeralBase(Preferences.numeralBase.getPreference(preferences));
        setMultiplicationSign(Preferences.multiplicationSign.getPreference(preferences));

        mathEngine.setPrecision(Preferences.Output.precision.getPreference(preferences));
        mathEngine.setNotation(Preferences.Output.notation.getPreference(preferences).id);
        mathEngine.setGroupingSeparator(Preferences.Output.separator.getPreference(preferences));

        bus.post(ChangedEvent.INSTANCE);
    }

    @Nonnull
    public String getMultiplicationSign() {
        return this.multiplicationSign;
    }

    public void setMultiplicationSign(@Nonnull String multiplicationSign) {
        this.multiplicationSign = multiplicationSign;
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (Preferences.getPreferenceKeys().contains(key)) {
            applyPreferences();
        }
    }

    public enum Notation implements PreferenceEntry {
        dec(Real.NumberFormat.FSE_NONE, R.string.cpp_number_format_dec),
        eng(Real.NumberFormat.FSE_ENG, R.string.cpp_number_format_eng),
        sci(Real.NumberFormat.FSE_SCI, R.string.cpp_number_format_sci);

        public final int id;
        @StringRes
        public final int name;

        Notation(int id, @StringRes int name) {
            this.id = id;
            this.name = name;
        }

        @NonNull
        @Override
        public CharSequence getName(@NonNull Context context) {
            return context.getString(name);
        }

        @NonNull
        @Override
        public CharSequence getId() {
            return name();
        }
    }

    public static class ChangedEvent {
        static final ChangedEvent INSTANCE = new ChangedEvent();

        private ChangedEvent() {
        }
    }

    public static class Preferences {
        public static final StringPreference<String> multiplicationSign = StringPreference.of("engine.multiplicationSign", "×");
        public static final StringPreference<NumeralBase> numeralBase = StringPreference.ofTypedValue("engine.numeralBase", "dec", EnumMapper.of(NumeralBase.class));
        public static final StringPreference<AngleUnit> angleUnit = StringPreference.ofTypedValue("engine.angleUnit", "deg", EnumMapper.of(AngleUnit.class));
        public static final Preference<Integer> version = IntegerPreference.of("engine.version", 2);
        private static final List<String> preferenceKeys = new ArrayList<>();

        static {
            preferenceKeys.add(multiplicationSign.getKey());
            preferenceKeys.add(numeralBase.getKey());
            preferenceKeys.add(angleUnit.getKey());
            preferenceKeys.add(Output.precision.getKey());
            preferenceKeys.add(Output.notation.getKey());
            preferenceKeys.add(Output.separator.getKey());
        }

        @Nonnull
        public static List<String> getPreferenceKeys() {
            return Collections.unmodifiableList(preferenceKeys);
        }

        @StringRes
        public static int angleUnitName(AngleUnit angleUnit) {
            switch (angleUnit) {
                case deg:
                    return R.string.cpp_deg;
                case rad:
                    return R.string.cpp_rad;
                case grad:
                    return R.string.cpp_grad;
                case turns:
                    return R.string.cpp_turns;
            }
            return 0;
        }

        @StringRes
        public static int numeralBaseName(NumeralBase numeralBase) {
            switch (numeralBase) {
                case bin:
                    return R.string.cpp_bin;
                case oct:
                    return R.string.cpp_oct;
                case dec:
                    return R.string.cpp_dec;
                case hex:
                    return R.string.cpp_hex;
            }
            return 0;
        }

        public static class Output {
            public static final StringPreference<Integer> precision = StringPreference.ofTypedValue("engine.output.precision", "5", NumberMapper.of(Integer.class));
            public static final StringPreference<Notation> notation = StringPreference.ofEnum("engine.output.notation", Notation.dec, Notation.class);
            public static final StringPreference<Character> separator = StringPreference.ofTypedValue("engine.output.separator", JsclMathEngine.GROUPING_SEPARATOR_DEFAULT, CharacterMapper.INSTANCE);
        }
    }
}
