package jforex.strategies.indicators;
 
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
 
import com.dukascopy.api.*;
import com.dukascopy.api.IEngine.OrderCommand;
import com.dukascopy.api.IIndicators.AppliedPrice;
import com.dukascopy.api.indicators.IIndicator;
import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Currency;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Set;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
 
@RequiresFullAccess
public class TrimaStrat implements IStrategy {
 
    private IEngine engine;
    private IContext context;
    private IConsole console;
    private IHistory history;
    private IIndicators indicators;
    private IAccount account;
    private int counter = 0;
    private IOrder order;
    @Configurable("Instrument")
    public Instrument instrument = Instrument.EURUSD;
    @Configurable("Period")
    public Period selectedPeriod = Period.TEN_MINS;
    @Configurable("SAR Acceleration")
    public double acceleration = 0.02;
    @Configurable("SAR Maximum")
    public double maximum = 0.2;
    @Configurable("Sound file")
    public File soundFile;
    @Configurable("Risk % of Equity")
    public double risk = 10;
    @Configurable("Applied price")
    public AppliedPrice appliedPrice = AppliedPrice.CLOSE;
    @Configurable("Offer side")
    public OfferSide offerSide = OfferSide.BID;
    @Configurable("Slippage")
    public double slippage = 0;
    @Configurable("Filter")
    public Filter filter = Filter.ALL_FLATS;
    @Configurable("Take profit pips")
    public int takeProfitPips = 0;
    @Configurable("Stop loss in pips")
    public int stopLossPips = 0;
    @Configurable("Open hour")
    public int openHour = 13;
    @Configurable("Open min")
    public int openMin = 0;
    @Configurable("Close hour")
    public int closeHour = 20;
    @Configurable("Close min")
    public int closeMin = 0;
 
    @Override
    public void onStart(IContext context) throws JFException {
        this.console = context.getConsole();
        this.indicators = context.getIndicators();
        this.history = context.getHistory();
        this.engine = context.getEngine();
        this.context = context;
        this.account = context.getAccount();
 
        IChart chart = context.getChart(instrument);
        if (chart != null) {
            chart.addIndicator(indicators.getIndicator("SAR"), new Object[]{acceleration, maximum});
        }
    }
 
    @Override
    public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException {
        if (period != this.selectedPeriod || instrument != this.instrument) {
            return;
        }
    }
 
    public void onTick(Instrument instrument, ITick tick) throws JFException {
        if (instrument != this.instrument) {
            return;
        }
 
        if (!isActive(order)) {
            order = null;
        } else if (order.equals(IOrder.State.CREATED)) {
            return;
        }
 
        // SIGNAL
        boolean sellSign = false;
        boolean buySign = false;
 
        IBar bar = history.getBar(instrument, selectedPeriod, offerSide, 1);
        double[] sar = indicators.sar(instrument, selectedPeriod, offerSide, acceleration, maximum, filter, 1, bar.getTime(), 1);
 
        double price1 = bar.getClose();
        double price0 = offerSide.equals(OfferSide.ASK) ? tick.getAsk() : tick.getBid();
 
  Object[] trima = indicators.calculateIndicator(instrument,selectedPeriod, new OfferSide[] {OfferSide.BID},"TRIMA_BANDS_2",new IIndicators.AppliedPrice[]{IIndicators.AppliedPrice.CLOSE},new Object[]{56,100,2}, 1); 
 double trima_high = (Double)trima[0]; 
 double trima_zerro=(Double)trima[1];
 double trima_low=(Double)trima[2]; 
 
        if (bar.getClose() > trima_high) {
            buySign = true;
        }
        if (bar.getClose()< trima_low) {
            sellSign = true;
        }
 
        // PLACE ORDER       
        if (buySign) {
            if (order == null || !order.isLong()) {
                playSound(soundFile, 1);
                closeOrder(order);
                order = submitOrder(OrderCommand.BUY, instrument);
            }
 
        } else if (sellSign) {
            if (order == null || order.isLong()) {
                playSound(soundFile, 1);
                closeOrder(order);
                order = submitOrder(OrderCommand.SELL, instrument);
            }
        }
    }
 
    private IOrder submitOrder(OrderCommand orderCmd, Instrument instr) throws JFException {
 
        double stopLossPrice = 0.0, takeProfitPrice = 0.0;
 
        ITick t = history.getLastTick(instr);
 
        // Calculating order price, stop loss and take profit prices
        if (orderCmd == OrderCommand.BUY) {
            if (stopLossPips > 0) {
                stopLossPrice = t.getBid() - getPipPrice(stopLossPips, instr);
            }
            if (takeProfitPips > 0) {
                takeProfitPrice = t.getBid() + getPipPrice(takeProfitPips, instr);
            }
        } else {
            if (stopLossPips > 0) {
                stopLossPrice = t.getAsk() + getPipPrice(stopLossPips, instr);
            }
            if (takeProfitPips > 0) {
                takeProfitPrice = t.getAsk() - getPipPrice(takeProfitPips, instr);
            }
        }
 
        double amount = XRate.getAmount(risk / 100, account.getEquity(), instrument, account, context);
        return engine.submitOrder(getLabel(instr), instr, orderCmd, amount, 0, slippage, stopLossPrice, takeProfitPrice);
    }
 
    private void closeOrder(IOrder order) throws JFException {
        if (order == null) {
            return;
        }
        if (order.getState() == IOrder.State.CREATED) {
            order.waitForUpdate();
        }
        if (order.getState() != IOrder.State.CLOSED && order.getState() != IOrder.State.CANCELED) {
            order.close();
            order.waitForUpdate();
            if (order.getState() == IOrder.State.FILLED) {
                // order OPENNED -> order.close() -> order FILLED -> recieves message ORDER_ALREADY_FILLED -> resend order.close()
                order.close();
            }
        }
    }
 
    private boolean isActive(IOrder order) throws JFException {
        if (order != null && order.getState() != IOrder.State.CLOSED && order.getState() != IOrder.State.CANCELED) {
            return true;
        }
        return false;
    }
 
    private double getPipPrice(double pips, Instrument instr) {
        return pips * instr.getPipValue();
    }
 
    private String getLabel(Instrument instr) {
        String label = instr.name();
        label = label + (counter++);
        label = label.toUpperCase();
        return label;
    }
 
    public boolean timeIsEqual(long time, int hour, int min) {
        Calendar cal = new GregorianCalendar();
        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
        cal.setTimeInMillis(time);
        cal.set(Calendar.HOUR_OF_DAY, hour);
        cal.set(Calendar.MINUTE, min);
 
        Calendar cal2 = new GregorianCalendar();
        cal2.setTimeZone(TimeZone.getTimeZone("GMT"));
        cal2.setTimeInMillis(cal.getTimeInMillis());
        cal2.add(Calendar.MINUTE, 5);
 
        if (cal.getTimeInMillis() <= time
                && time <= cal2.getTimeInMillis()) {
            return true;
        }
        return false;
    }
 
    public boolean isRightTime(long time, int fromHour, int fromMin, int toHour, int toMin) {
        Calendar cal = new GregorianCalendar();
        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
        cal.setTimeInMillis(time);
        cal.set(Calendar.HOUR_OF_DAY, fromHour);
        cal.set(Calendar.MINUTE, fromMin);
 
        Calendar cal2 = new GregorianCalendar();
        cal2.setTimeZone(TimeZone.getTimeZone("GMT"));
        cal2.setTimeInMillis(time);
        cal2.set(Calendar.HOUR_OF_DAY, toHour);
        cal2.set(Calendar.MINUTE, toMin);
 
        if (cal.getTimeInMillis() <= time
                && time <= cal2.getTimeInMillis()) {
            return true;
        }
        return false;
    }
 
    public void updateTrailingStopLoss(ITick tick, IOrder pOrder, double pTriggerPips, double pStopLossPips) throws JFException {
 
        if (pStopLossPips > 0 && pOrder != null && pOrder.getState() == IOrder.State.FILLED) {
 
            Instrument instr = pOrder.getInstrument();
 
            double newStop;
            double openPrice = pOrder.getOpenPrice();
            double currentStopLoss = pOrder.getStopLossPrice();
 
            // (START) trailing stop loss is activated when price is higher than oper price + trailingTrigger pips
            // (TRAILING STOP) if price moves further up (for BUY order), stop loss is updated to stopLossPips
 
            if (pOrder.isLong()) { // long side order               
                if ((currentStopLoss == 0.0 || tick.getBid() > currentStopLoss + getPipPrice(pStopLossPips, instr))
                        && tick.getBid() > openPrice + getPipPrice(pTriggerPips, instr)) {
                    // trailing stop loss
                    newStop = tick.getBid() - getPipPrice(pStopLossPips, instr);
                    newStop = getRoundedPrice(newStop, instr);
 
                    if (currentStopLoss != newStop) {
                        pOrder.setStopLossPrice(newStop);
                        return;
                    }
                }
 
            } else { // short side order           
                if ((currentStopLoss == 0.0 || tick.getAsk() < currentStopLoss - getPipPrice(pStopLossPips, instr))
                        && tick.getAsk() < openPrice - getPipPrice(pTriggerPips, instr)) {
 
                    // trailing stop loss
                    newStop = tick.getAsk() + getPipPrice(pStopLossPips, instr);
                    newStop = getRoundedPrice(newStop, instr);
 
                    if (currentStopLoss != newStop) {
                        pOrder.setStopLossPrice(newStop);
                        return;
                    }
                }
            }
        }
    }
 
    private double getRoundedPrice(double price, Instrument instr) {
        BigDecimal bd = new BigDecimal(price);
        bd = bd.setScale(instr.getPipScale() + 1, RoundingMode.HALF_UP);
        return bd.doubleValue();
    }
 
    private double getRoundedPipCount(double pips) {
        BigDecimal bd = new BigDecimal(pips);
        bd = bd.setScale(1, RoundingMode.HALF_UP);
        return bd.doubleValue();
    }
 
    public void onMessage(IMessage message) throws JFException {
    }
 
    public void onAccount(IAccount account) throws JFException {
    }
 
    public void onStop() throws JFException {
    }
 
    /**************** debug print functions ***********************/
    private void print(Object... o) {
        for (Object ob : o) {
            //console.getOut().print(ob + "  ");
            if (ob instanceof Double) {
                print2(toStr((Double) ob));
            } else if (ob instanceof double[]) {
                print((double[]) ob);
            } else if (ob instanceof double[]) {
                print((double[][]) ob);
            } else if (ob instanceof Long) {
                print2(toStr((Long) ob));
            } else if (ob instanceof IBar) {
                print2(toStr((IBar) ob));
            } else {
                print2(ob);
            }
            print2(" ");
        }
        console.getOut().println();
    }
 
    private void print(Object o) {
        console.getOut().println(o);
    }
 
    private void print2(Object o) {
        console.getOut().print(o);
    }
 
    private void print(double d) {
        print(toStr(d));
    }
 
    private void print(double[] arr) {
        print(toStr(arr));
    }
 
    private void print(double[][] arr) {
        print(toStr(arr));
    }
 
    private void print(IBar bar) {
        print(toStr(bar));
    }
 
    private void printIndicatorInfos(IIndicator ind) {
        for (int i = 0; i < ind.getIndicatorInfo().getNumberOfInputs(); i++) {
            print(ind.getIndicatorInfo().getName() + " Input " + ind.getInputParameterInfo(i).getName() + " " + ind.getInputParameterInfo(i).getType());
        }
        for (int i = 0; i < ind.getIndicatorInfo().getNumberOfOptionalInputs(); i++) {
            print(ind.getIndicatorInfo().getName() + " Opt Input " + ind.getOptInputParameterInfo(i).getName() + " " + ind.getOptInputParameterInfo(i).getType());
        }
        for (int i = 0; i < ind.getIndicatorInfo().getNumberOfOutputs(); i++) {
            print(ind.getIndicatorInfo().getName() + " Output " + ind.getOutputParameterInfo(i).getName() + " " + ind.getOutputParameterInfo(i).getType());
        }
        console.getOut().println();
    }
 
    public static String toStr(double[] arr) {
        String str = "";
        for (int r = 0; r < arr.length; r++) {
            str += "[" + r + "] " + (new DecimalFormat("#.#######")).format(arr[r]) + "; ";
        }
        return str;
    }
 
    public static String toStr(double[][] arr) {
        String str = "";
        if (arr == null) {
            return "null";
        }
        for (int r = 0; r < arr.length; r++) {
            for (int c = 0; c < arr[r].length; c++) {
                str += "[" + r + "][" + c + "] " + (new DecimalFormat("#.#######")).format(arr[r][c]);
            }
            str += "; ";
        }
        return str;
    }
 
    public String toStr(double d) {
        return (new DecimalFormat("#.#######")).format(d);
    }
 
    public String toStr(Long time) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {
 
            {
                setTimeZone(TimeZone.getTimeZone("GMT"));
            }
        };
        return sdf.format(time);
    }
 
    private String toStr(IBar bar) {
        return toStr(bar.getTime()) + "  O:" + bar.getOpen() + " C:" + bar.getClose() + " H:" + bar.getHigh() + " L:" + bar.getLow();
    }
 
    private void printTime(Long time) {
        console.getOut().println(toStr(time));
    }
 
    private void playSound(File wavFile, int repeats) {
        try {
            print(wavFile);
            if (wavFile == null || !wavFile.exists()) {
                return;
            }
            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(wavFile);
            AudioFormat af = audioInputStream.getFormat();
            int nSize = (int) (af.getFrameSize() * audioInputStream.getFrameLength());
            byte[] audio = new byte[nSize];
            DataLine.Info info = new DataLine.Info(Clip.class, af, nSize);
            audioInputStream.read(audio, 0, nSize);
 
            for (int i = 0; i < repeats; i++) {
                Clip clip = (Clip) AudioSystem.getLine(info);
                clip.open(af, audio, 0, nSize);
                clip.start();
            }
        } catch (Exception e) {
            console.getErr().println(e.getMessage());
        }
    }
}
 
class XRate {
 
    private IHistory history;
    private IContext context;
    private IConsole console;
 
    public XRate(IContext context) {
        this.history = context.getHistory();
        this.context = context;
        this.console = context.getConsole();
    }
    private String[] mainCurrencies = {"USD", "GBP", "AUD", "CAD", "CHF", "GBP", "HKD", "NZD"};
 
    private double getExchangeRate(Currency c1, Currency c2) throws JFException {
 
        Instrument instr = getInstrument(c1.getCurrencyCode(), c2.getCurrencyCode());
 
        if (instr != null) {
            subscribe(instr);
            return getPrice(instr);
        } else {
            instr = getInstrument(c2.getCurrencyCode(), c1.getCurrencyCode());
            if (instr != null) {
                subscribe(instr);
                return 1 / getPrice(instr);
            }
        }
 
        double EURCUR1 = exchangeRateEUR(c1);
        double EURCUR2 = exchangeRateEUR(c2);
        return EURCUR2 / EURCUR1;
    }
 
    private double exchangeRateEUR(Currency CUR) throws JFException {
 
        if (CUR.equals(Currency.getInstance("EUR"))) {
            return 1;
        }
 
        Instrument instr2 = null;
        Instrument instr = getInstrument("EUR", CUR.getCurrencyCode());
 
        if (instr == null) {
            for (String mainCUR : mainCurrencies) {
 
                if (mainCUR.equals(CUR.getCurrencyCode())) {
                    instr = getInstrument("EUR", mainCUR);
                    break;
                }
 
                instr = getInstrument(mainCUR, CUR.getCurrencyCode());
                if (instr != null) {
                    instr2 = getInstrument("EUR", mainCUR);
                    if (instr2 == null) {
                        throw new JFException("Error in currency exchange rate calculation.");
                    }
                    break;
                }
            }
        }
 
        if (instr != null) {
            if (instr2 == null) {
                return getPrice(instr); // EUR <-> MAIN_CUR
            } else {
                return getPrice(instr) * getPrice(instr2); // EUR <-> MAIN_CUR + MAIN_CUR <-> CUR
            }
        } else {
            if (CUR.equals(Currency.getInstance("XAU")) || CUR.equals(Currency.getInstance("XAG"))) {
                instr = Instrument.EURUSD;
                instr2 = getInstrument(CUR.getCurrencyCode(), "USD");
            } else {
                throw new JFException("Error in currency exchange rate calculation.");
            }
            subscribe(instr);
            subscribe(instr2);
 
            // EURUSD * (1 / XXXUSD) = EURUSD * USDXXX = EURXXX
            return getPrice(instr) * (1 / getPrice(instr2)); // EUR <-> MAIN_CUR + MAIN_CUR <-> CUR
        }
    }
 
    private Instrument getInstrument(String c1, String c2) {
 
        if (Instrument.contains(c1 + "/" + c2)) {
            return Instrument.valueOf(c1 + c2);
        }
        return null;
    }
 
    private void subscribe(Instrument instr) throws JFException {
        Set<Instrument> instruments = context.getSubscribedInstruments();
        Set<Instrument> newInstr = new HashSet<Instrument>();
        if (!instruments.contains(instr)) {
 
            newInstr.add(instr);
            context.setSubscribedInstruments(newInstr);
 
            int i = 10;
            while (!context.getSubscribedInstruments().containsAll(newInstr) && i > 0) {
                try {
                    console.getOut().println("Instruments not subscribed yet " + i);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    console.getOut().println(e.getMessage());
                }
                i--;
            }
 
        }
    }
 
    private double getPrice(Instrument instr) throws JFException {
        subscribe(instr);
        ITick tick = history.getLastTick(instr);
        return (tick.getAsk() + tick.getBid()) / 2;
    }
 
    static public double getAmount(double risk, Instrument instrument, double slPips, IAccount account, IContext context) throws JFException {
        return getAmount(risk, account.getEquity(), instrument, slPips, account, context);
    }
 
    // stop loss pips -> amount
    static public double getAmount(double risk, double totalAmount, Instrument instrument, double slPips, IAccount account, IContext context) throws JFException {
        // !!! USD EUR and JPY in variable names are used only for simplicity
        // this method is universal and USD EUR and JPY can be any other currencies
        // EURJPY - instrument, USD account currency
 
        if (risk < 0.0 || risk > 1.0) {
            throw new JFException("Risk must be from 0.0 to 1.0.");
        }
        double riskUSD = totalAmount * risk;
        double pipJPY = instrument.getPipValue(); // USDJPY pip value in JPY
 
        XRate xrate = new XRate(context);
        double USDJPY = xrate.getExchangeRate(account.getCurrency(), instrument.getSecondaryCurrency());
        double riskJPY = riskUSD * USDJPY;
        double amountEUR = riskJPY / (slPips * pipJPY);
 
        return amountEUR / 1000000;
    }
 
    // percent of total amount
    static public double getAmount(double risk, double totalAmount, Instrument instrument, IAccount account, IContext context) throws JFException {
        if (risk < 0.0 || risk > 1.0) {
            throw new JFException("Risk must be from 0.0 to 1.0.");
        }
        double riskUSD = totalAmount * risk;
        double pipJPY = instrument.getPipValue(); // USDJPY pip value in JPY
 
        XRate xrate = new XRate(context);
        double USDJPY = xrate.getExchangeRate(account.getCurrency(), instrument.getSecondaryCurrency());
        double riskJPY = riskUSD * USDJPY * account.getLeverage();
 
        return riskJPY / 1000000;
    }
 
    static void print(double d, IContext context) {
        context.getConsole().getOut().println((new DecimalFormat("#.#######")).format(d));
    }
}