/*
 *  /**
 *  * Copyright (C) 2017  Grbl Controller Contributors
 *  *
 *  * This program is free software; you can redistribute it and/or modify
 *  * it under the terms of the GNU General Public License as published by
 *  * the Free Software Foundation; either version 2 of the License, or
 *  * (at your option) any later version.
 *  *
 *  * This program is distributed in the hope that it will be useful,
 *  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  * GNU General Public License for more details.
 *  *
 *  * You should have received a copy of the GNU General Public License along
 *  * with this program; if not, write to the Free Software Foundation, Inc.,
 *  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *  * <http://www.gnu.org/licenses/>
 *
 */

package in.co.gorest.grblcontroller.service;

import android.app.IntentService;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PowerManager;
import android.os.Process;
import androidx.core.app.NotificationCompat;
import android.util.Log;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import in.co.gorest.grblcontroller.R;
import in.co.gorest.grblcontroller.events.GrblErrorEvent;
import in.co.gorest.grblcontroller.events.GrblOkEvent;
import in.co.gorest.grblcontroller.events.StreamingCompleteEvent;
import in.co.gorest.grblcontroller.events.StreamingStartedEvent;
import in.co.gorest.grblcontroller.events.UiToastEvent;
import in.co.gorest.grblcontroller.helpers.NotificationHelper;
import in.co.gorest.grblcontroller.listeners.FileSenderListener;
import in.co.gorest.grblcontroller.listeners.MachineStatusListener;
import in.co.gorest.grblcontroller.model.Constants;
import in.co.gorest.grblcontroller.model.GcodeCommand;
import in.co.gorest.grblcontroller.util.GrblUtils;


public class FileStreamerIntentService extends IntentService{

    private static final String TAG = FileStreamerIntentService.class.getSimpleName();

    public static final String CHECK_MODE_ENABLED = "CHECK_MODE_ENABLED";
    public static final String SERIAL_CONNECTION_TYPE = "SERIAL_CONNECTION_TYPE";
    private static int MAX_RX_SERIAL_BUFFER = Constants.DEFAULT_SERIAL_RX_BUFFER - 3;
    private static int CURRENT_RX_SERIAL_BUFFER = 0;

    private final LinkedList<Integer> activeCommandSizes = new LinkedList<>();
    private final BlockingQueue<Integer> completedCommands = new ArrayBlockingQueue<>(Constants.DEFAULT_SERIAL_RX_BUFFER);

    private static volatile boolean isServiceRunning = false;
    private static volatile boolean shouldContinue = true;

    public synchronized static boolean getIsServiceRunning(){ return isServiceRunning; }
    private synchronized static void setIsServiceRunning(boolean running){ isServiceRunning = running; }

    public synchronized static boolean getShouldContinue(){ return shouldContinue; }
    public synchronized static void setShouldContinue(boolean b){ shouldContinue = b; }

    private FileSenderListener fileSenderListener;
    private MachineStatusListener machineStatusListener;
    private final Timer jobTimer = new Timer();

    public FileStreamerIntentService() {
        super(FileStreamerIntentService.class.getName());
    }

    @Override
    public void onCreate(){
        super.onCreate();
        EventBus.getDefault().register(this);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
        clearBuffers();
        jobTimer.cancel();
        setIsServiceRunning(false);
        if(fileSenderListener != null) fileSenderListener.setStatus(FileSenderListener.STATUS_IDLE);
        stopForeground(true);
        EventBus.getDefault().unregister(this);
    }

    @Override
    protected void onHandleIntent(Intent intent){
        fileSenderListener = FileSenderListener.getInstance();
        if(fileSenderListener.getGcodeFile() == null){
            EventBus.getDefault().post(new UiToastEvent(getString(R.string.text_no_gcode_file_selected), true, true));
            return;
        }

        machineStatusListener = MachineStatusListener.getInstance();

        MachineStatusListener.CompileTimeOptions compileTimeOptions = machineStatusListener.getCompileTimeOptions();
        if(compileTimeOptions.serialRxBuffer > 0) MAX_RX_SERIAL_BUFFER = compileTimeOptions.serialRxBuffer - 3;
        Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);

        boolean isCheckMode = intent.getBooleanExtra(CHECK_MODE_ENABLED, false);
        String defaultConnectionType = intent.getStringExtra(SERIAL_CONNECTION_TYPE);

        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        PowerManager.WakeLock wakeLock = null;
        if (powerManager != null) {
            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "GrblController:WakeLockTag");
            wakeLock.acquire(24*60*60*1000);
        }


        clearBuffers();
        fileSenderListener.setRowsSent(0);
        fileSenderListener.setJobStartTime(System.currentTimeMillis());

        try {
            jobTimer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    int elapsedTimeSeconds = (int) (System.currentTimeMillis() - fileSenderListener.getJobStartTime())/1000;
                    fileSenderListener.setElapsedTime(String.format(Locale.US ,"%02d:%02d:%02d", elapsedTimeSeconds / 3600, (elapsedTimeSeconds % 3600) / 60, (elapsedTimeSeconds % 60)));
                }
            }, 0, 1000);
        }catch (IllegalStateException ignored){

        }

        fileSenderListener.setStatus(FileSenderListener.STATUS_STREAMING);
        setIsServiceRunning(true);
        EventBus.getDefault().post(new StreamingStartedEvent());

        if(isCheckMode){
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1){
                startForeground(Constants.FILE_STREAMING_NOTIFICATION_ID, getNotification(getString(R.string.text_file_checking_started), fileSenderListener.getGcodeFile().getName()));
            }

            if(defaultConnectionType != null && defaultConnectionType.equals(Constants.SERIAL_CONNECTION_TYPE_BLUETOOTH)){
                this.checkGcodeFile();
            }else{
                this.startStreaming(555);
            }
        }else{
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1){
                startForeground(Constants.FILE_STREAMING_NOTIFICATION_ID, getNotification(getString(R.string.text_file_streaming_started), fileSenderListener.getGcodeFile().getName()));
            }

            this.startStreaming(5);
        }

        this.waitUntilBufferRunOut(true);

        jobTimer.cancel();
        fileSenderListener.setJobEndTime(System.currentTimeMillis());
        fileSenderListener.setStatus(FileSenderListener.STATUS_IDLE);

        if(!getShouldContinue() && machineStatusListener.getLaserModeEnabled()){
            // Stop the laser in case of emergency button is pressed
			EventBus.getDefault().post(new GcodeCommand("M5"));
        }

        clearBuffers();
        setIsServiceRunning(false);

        StreamingCompleteEvent streamingCompleteEvent = new StreamingCompleteEvent("Streaming Completed");
        streamingCompleteEvent.setFileName(fileSenderListener.getGcodeFileName());
        streamingCompleteEvent.setRowsSent(fileSenderListener.getRowsSent());
        streamingCompleteEvent.setTimeMillis(fileSenderListener.getJobEndTime() - fileSenderListener.getJobStartTime());
        streamingCompleteEvent.setTimeTaken(fileSenderListener.getElapsedTime());

        EventBus.getDefault().post(streamingCompleteEvent);

        if(wakeLock != null){
            try{
                wakeLock.release();
            }catch (RuntimeException ignored){}
        }

        stopSelf();
    }

    private void startStreaming(int statusUpdateInterval){

        BufferedReader br; String sCurrentLine;

        try{
            br = new BufferedReader(new FileReader(fileSenderListener.getGcodeFile()));
            int linesSent = 0;
            GcodeCommand gcodeCommand = new GcodeCommand();
            while ((sCurrentLine = br.readLine()) != null) {
                if(!shouldContinue) break;

                gcodeCommand.setCommand(sCurrentLine);
                if(gcodeCommand.getSize() > 1){

                    if(gcodeCommand.getHasRomAccess()){
                        this.waitUntilBufferRunOut(true);
                        streamLine(gcodeCommand);
                        this.waitUntilBufferRunOut();
                        streamLine(new GcodeCommand(GrblUtils.GRBL_VIEW_PARSER_STATE_COMMAND));
                    }else{
                        streamLine(gcodeCommand);
                    }

                    linesSent++;
                }

                if(linesSent%statusUpdateInterval == 0){
                    fileSenderListener.setRowsSent(linesSent);
                }

            }

            br.close();
            fileSenderListener.setRowsSent(linesSent);

        }catch (IOException | NullPointerException ignored){}

    }

    private void checkGcodeFile(){

        try{

            BufferedReader br = new BufferedReader(new FileReader(fileSenderListener.getGcodeFile()));
            int linesSent = 0;
            String sCurrentLine;
            GcodeCommand gcodeCommand = new GcodeCommand();

            while ((sCurrentLine = br.readLine()) != null) {
                if(!shouldContinue) break;
                gcodeCommand.setCommand(sCurrentLine);
                if(gcodeCommand.getCommandString().length() > 0){
                    EventBus.getDefault().post(gcodeCommand);
                    linesSent++;
                }
                if(linesSent%555 == 0) fileSenderListener.setRowsSent(linesSent);
            }
            br.close();

            fileSenderListener.setRowsSent(linesSent);

        }catch (IOException | NullPointerException e){
            Log.e(TAG, e.getMessage(), e);
        }
    }

    private void waitUntilBufferRunOut(){
        this.waitUntilBufferRunOut(false);
    }

    private void waitUntilBufferRunOut(boolean dwell){
        if(dwell) streamLine(new GcodeCommand("G4P0.01"));

        while(CURRENT_RX_SERIAL_BUFFER > 0){
            try {
                completedCommands.take();
                if(activeCommandSizes.size() > 0) CURRENT_RX_SERIAL_BUFFER -= activeCommandSizes.removeFirst();
            } catch (Exception e) {
                Log.e(TAG, e.getMessage(), e);
                return;
            }
        }
    }

    private void streamLine(GcodeCommand gcodeCommand){

        if(machineStatusListener.getSingleStepMode()){
            try {
                EventBus.getDefault().post(gcodeCommand);
                completedCommands.take();
            } catch (InterruptedException ignored) {}
        }else{
            // Wait until there is room, if necessary.
            while (MAX_RX_SERIAL_BUFFER < (CURRENT_RX_SERIAL_BUFFER + gcodeCommand.getSize())) {
                try {
                    completedCommands.take();
                    if(activeCommandSizes.size() > 0) CURRENT_RX_SERIAL_BUFFER -= activeCommandSizes.removeFirst();
                } catch (InterruptedException e) {
                    Log.e(TAG, e.getMessage(), e);
                    return;
                }
            }

            if(getShouldContinue()){
                activeCommandSizes.offer(gcodeCommand.getSize());
                CURRENT_RX_SERIAL_BUFFER += gcodeCommand.getSize();
                EventBus.getDefault().post(gcodeCommand);
            }
        }
    }

    private void clearBuffers(){
        CURRENT_RX_SERIAL_BUFFER = 0;
        if(activeCommandSizes.size() > 0) activeCommandSizes.clear();
        if(completedCommands.size() > 0) completedCommands.clear();
    }

    private Notification getNotification(String title, String message){
        return new NotificationCompat.Builder(getApplicationContext(), NotificationHelper.CHANNEL_SERVICE_ID)
                .setContentTitle(title)
                //.setContentText(message)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
                .setSmallIcon(R.drawable.ic_stat_ic_notification)
                .setColor(getResources().getColor(R.color.colorPrimary))
                .setAutoCancel(true).build();
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onGrblOkEvent(GrblOkEvent event){
        try {
            completedCommands.put(1);
        } catch (InterruptedException ignored) {}
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onGrblErrorEvent(GrblErrorEvent event){
        setShouldContinue(event.getErrorCode() == 20 && machineStatusListener.getIgnoreError20());
    }

}