/*
* 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.SharedPreferences;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import jscl.JsclArithmeticException;
import jscl.MathEngine;
import jscl.NumeralBase;
import jscl.math.Generic;
import jscl.math.function.Constants;
import jscl.math.function.IConstant;
import jscl.text.ParseInterruptedException;
import org.solovyev.android.Check;
import org.solovyev.android.calculator.calculations.*;
import org.solovyev.android.calculator.functions.FunctionsRegistry;
import org.solovyev.android.calculator.jscl.JsclOperation;
import org.solovyev.android.calculator.variables.CppVariable;
import org.solovyev.common.msg.ListMessageRegistry;
import org.solovyev.common.msg.Message;
import org.solovyev.common.msg.MessageRegistry;
import org.solovyev.common.msg.MessageType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.measure.converter.ConversionException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
@Singleton
public class Calculator implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final long NO_SEQUENCE = -1;
@Nonnull
private static final AtomicLong SEQUENCER = new AtomicLong(NO_SEQUENCE);
@Nonnull
private final SharedPreferences preferences;
@Nonnull
final Bus bus;
@Nonnull
private final Executor background;
private volatile boolean calculateOnFly = true;
@Inject
Editor editor;
@Inject
Engine engine;
@Inject
ToJsclTextProcessor preprocessor;
@Inject
public Calculator(@Nonnull SharedPreferences preferences, @Nonnull Bus bus, @Named(AppModule.THREAD_BACKGROUND) @Nonnull Executor background) {
this.preferences = preferences;
this.bus = bus;
this.background = background;
bus.register(this);
preferences.registerOnSharedPreferenceChangeListener(this);
}
@Nonnull
private static String convert(@Nonnull Generic generic, @Nonnull NumeralBase to) throws ConversionException {
final BigInteger value = generic.toBigInteger();
if (value == null) {
throw new ConversionException();
}
return to.toString(value);
}
public void evaluate() {
final EditorState state = editor.getState();
evaluate(JsclOperation.numeric, state.getTextString(), state.sequence);
}
public void simplify() {
final EditorState state = editor.getState();
evaluate(JsclOperation.simplify, state.getTextString(), state.sequence);
}
public long evaluate(@Nonnull final JsclOperation operation, @Nonnull final String expression,
final long sequence) {
background.execute(new Runnable() {
@Override
public void run() {
evaluateAsync(sequence, operation, expression);
}
});
return sequence;
}
public void init(@Nonnull Executor init) {
engine.init(init);
setCalculateOnFly(Preferences.Calculations.calculateOnFly.getPreference(preferences));
}
public boolean isCalculateOnFly() {
return calculateOnFly;
}
public void setCalculateOnFly(boolean calculateOnFly) {
if (this.calculateOnFly != calculateOnFly) {
this.calculateOnFly = calculateOnFly;
if (this.calculateOnFly) {
evaluate();
}
}
}
private void evaluateAsync(long sequence, @Nonnull JsclOperation o, @Nonnull String e) {
evaluateAsync(sequence, o, e, new ListMessageRegistry());
}
private void evaluateAsync(long sequence,
@Nonnull JsclOperation o,
@Nonnull String e,
@Nonnull MessageRegistry mr) {
e = e.trim();
if (TextUtils.isEmpty(e)) {
bus.post(new CalculationFinishedEvent(o, e, sequence));
return;
}
PreparedExpression pe = null;
try {
pe = prepare(e);
try {
final MathEngine mathEngine = engine.getMathEngine();
mathEngine.setMessageRegistry(mr);
final Generic result = o.evaluateGeneric(pe.value, mathEngine);
// NOTE: toString() method must be called here as ArithmeticOperationException may occur in it (just to avoid later check!)
//noinspection ResultOfMethodCallIgnored
result.toString();
final String stringResult = o.getFromProcessor(engine).process(result);
bus.post(new CalculationFinishedEvent(o, e, sequence, result, stringResult, collectMessages(mr)));
} catch (JsclArithmeticException exception) {
bus.post(new CalculationFailedEvent(o, e, sequence, exception));
}
} catch (ArithmeticException exception) {
onException(sequence, o, e, mr, pe, new ParseException(e, new CalculatorMessage(CalculatorMessages.msg_001, MessageType.error, exception.getMessage())));
} catch (StackOverflowError exception) {
onException(sequence, o, e, mr, pe, new ParseException(e, new CalculatorMessage(CalculatorMessages.msg_002, MessageType.error)));
} catch (jscl.text.ParseException exception) {
onException(sequence, o, e, mr, pe, new ParseException(exception));
} catch (ParseInterruptedException exception) {
bus.post(new CalculationCancelledEvent(o, e, sequence));
} catch (ParseException exception) {
onException(sequence, o, e, mr, pe, exception);
} catch (RuntimeException exception) {
onException(sequence, o, e, mr, pe, new ParseException(e, new CalculatorMessage(CalculatorMessages.syntax_error, MessageType.error)));
}
}
@Nonnull
private List<Message> collectMessages(@Nonnull MessageRegistry mr) {
if (mr.hasMessage()) {
try {
final List<Message> messages = new ArrayList<>();
while (mr.hasMessage()) {
messages.add(mr.getMessage());
}
return messages;
} catch (Throwable exception) {
// several threads might use the same instance of MessageRegistry, as no proper synchronization is done
// catch Throwable here
Log.e("Calculator", exception.getMessage(), exception);
}
}
return Collections.emptyList();
}
@Nonnull
public PreparedExpression prepare(@Nonnull String expression) throws ParseException {
return preprocessor.process(expression);
}
private void onException(long sequence,
@Nonnull JsclOperation operation,
@Nonnull String e,
@Nonnull MessageRegistry mr,
@Nullable PreparedExpression pe,
@Nonnull ParseException parseException) {
if (operation == JsclOperation.numeric
&& pe != null
&& pe.hasUndefinedVariables()) {
evaluateAsync(sequence, JsclOperation.simplify, e, mr);
return;
}
bus.post(new CalculationFailedEvent(operation, e, sequence, parseException));
}
public void convert(@Nonnull final DisplayState state, @Nonnull final NumeralBase to) {
final Generic value = state.getResult();
Check.isNotNull(value);
final NumeralBase from = engine.getMathEngine().getNumeralBase();
if (from == to) {
return;
}
background.execute(new Runnable() {
@Override
public void run() {
try {
final String result = convert(value, to);
bus.post(new ConversionFinishedEvent(result, to, state));
} catch (ConversionException e) {
bus.post(new ConversionFailedEvent(state));
}
}
});
}
public boolean canConvert(@Nonnull Generic generic, @NonNull NumeralBase from, @Nonnull NumeralBase to) {
if(from == to) {
return false;
}
try {
convert(generic, to);
return true;
} catch (ConversionException e) {
return false;
}
}
@Subscribe
public void onEditorChanged(@Nonnull Editor.ChangedEvent e) {
if (!calculateOnFly) {
return;
}
if (!e.shouldEvaluate()) {
return;
}
evaluate(JsclOperation.numeric, e.newState.getTextString(), e.newState.sequence);
}
@Subscribe
public void onDisplayChanged(@Nonnull Display.ChangedEvent e) {
final DisplayState newState = e.newState;
if (!newState.valid) {
return;
}
final String text = newState.text;
if (TextUtils.isEmpty(text)) {
return;
}
updateAnsVariable(text);
}
void updateAnsVariable(@NonNull String value) {
final VariablesRegistry variablesRegistry = engine.getVariablesRegistry();
final IConstant variable = variablesRegistry.get(Constants.ANS);
final CppVariable.Builder b = variable != null ? CppVariable.builder(variable) : CppVariable.builder(Constants.ANS);
b.withValue(value);
b.withSystem(true);
b.withDescription(CalculatorMessages.getBundle().getString(CalculatorMessages.ans_description));
variablesRegistry.addOrUpdate(b.build().toJsclConstant(), variable);
}
@Subscribe
public void onFunctionAdded(@Nonnull FunctionsRegistry.AddedEvent event) {
evaluate();
}
@Subscribe
public void onFunctionsChanged(@Nonnull FunctionsRegistry.ChangedEvent event) {
evaluate();
}
@Subscribe
public void onFunctionsRemoved(@Nonnull FunctionsRegistry.RemovedEvent event) {
evaluate();
}
@Subscribe
public void onVariableRemoved(@NonNull VariablesRegistry.RemovedEvent e) {
evaluate();
}
@Subscribe
public void onVariableAdded(@NonNull VariablesRegistry.AddedEvent e) {
evaluate();
}
@Subscribe
public void onVariableChanged(@NonNull VariablesRegistry.ChangedEvent e) {
if (!e.newVariable.getName().equals(Constants.ANS)) {
evaluate();
}
}
@Override
public void onSharedPreferenceChanged(@Nonnull SharedPreferences prefs, @Nonnull String key) {
if (Preferences.Calculations.calculateOnFly.getKey().equals(key)) {
setCalculateOnFly(Preferences.Calculations.calculateOnFly.getPreference(prefs));
}
}
public static long nextSequence() {
return SEQUENCER.incrementAndGet();
}
}