import SBC.*;
import SIP.SIP_Request;
import SIP.SIP_Response;
import SIP.jSIPClient;
import SIP.javaSipEvents;
import Webpage.*;
import code.common;
import com.google.gson.JsonSyntaxException;
import org.pmw.tinylog.Logger;

import java.io.File;
import java.text.MessageFormat;
import java.util.*;

import static code.common.*;

public class Main {
    private static jSIPClient client;
    private static WebServer webserver;
    private static SocketioServer socketioserver;
    private static GpioInput callButton;
    private static GpioInput hangupButton;
    private static GpioOutput pilotLight;
    private static GpioOutput networkLight;
    private static GpioOutput callLight;
    private static GpioOutput Buzzer;
    private static Timer timer;
    private static Timer system_monitoring_timer;
    private static TimerTask callLightTask;
    private static TimerTask BuzzerTask;
    private static SIP_Request incomingRequest;
    private static SIP_Request oncallRequest;
    private static SIP_Response incomingResponse;
    private static SIP_Response oncallResponse;
    private static String SipStatus = "Idle";
    private static int cpuTemperature;
    private static RamInformation ramInformation;
    private static ProcessorStatus[] previousCpuInfo;
    private static final Map<String, Integer> cpuUsage = new HashMap<>();
    private static NetworkTransmitReceiveInfo[] previousNetworkInfo;
    private static final Map<String, String> networkTX = new HashMap<>();
    private static final Map<String, String> networkRX = new HashMap<>();

    public static void main(String[] args) {
        common.ExtractProperties(currentDir,"config.properties", false);
        Properties config = common.LoadProperties(currentDir,"config.properties");

        // Timer Section
        timer =  new Timer();

        // SIP Section
        client = new jSIPClient(config);
        client.SetJavaSipEvent(new javaSipEvents() {
            @Override
            public void Registering(SIP_Request req) {
                Logger.info("Registering to SIP Server, Request: {}",req);
                Buzzer_Off();
                callLight_Registering();
                SipStatus = "Registering";
            }

            @Override
            public void RegisterSuccesful(SIP_Response resp) {
                Logger.info("Registered to SIP Server, Response: {}",resp);
                Buzzer_Off();
                callLight_Idle();
                Logger.info("Calling callLight idle from RegisterSuccesful");
                SipStatus = "Idle";
                CallTest("100");
            }

            @Override
            public void RegisterFailed(SIP_Response resp) {
                Logger.info("Failed to register to SIP Server, Response: {}",resp);
                Buzzer_Off();
                SipStatus = "Register Failed";
            }

            @Override
            public void IncomingCall(SIP_Request req, SIP_Response resp) {
                Logger.info("Incoming Call, Request: {}, Response: {}",req,resp);
                callLight_IncomingCall();
                Buzzer_IncomingCall();
                incomingRequest = req;
                incomingResponse = resp;
                oncallRequest = null;
                oncallResponse = null;
                SipStatus = "Incoming Call from "+req.CallID;
                //client.AcceptIncomingCall(req);
            }

            @Override
            public void RemoteHangUp(SIP_Request req) {
                Logger.info("Remote Hangup, Request: {}",req);
                callLight_Idle();
                Logger.info("Calling callLight idle from RemoteHangUp");
                Buzzer_Off();

                client.HangUp();
                SipStatus = "Remote Hangup from "+req.CallID;
                incomingRequest = null;
                incomingResponse = null;
                oncallRequest = null;
                oncallResponse = null;

            }

            @Override
            public void Ringing(SIP_Response resp) {
                Logger.info("Ringing, Response: {}",resp);
                SipStatus = "Incoming Call from "+resp.CallID;
            }

            @Override
            public void CalleePickup(SIP_Response resp) {
                Logger.info("Callee Pickup, Response: {}",resp);
                SipStatus = "Communication with "+resp.CallID;
            }
        });
        client.Connect();


        // Web Server Section
        webserver = new WebServer(config);
        webserver.Start();
        socketioserver = new SocketioServer("0.0.0.0", 9092);
        socketioserver.Start();
        socketioserver.setOnRequest(req ->{
            // Ada request dari web
            SocketioResponse resp;
            if (networkLight!=null && networkLight.isInitialized()) networkLight.BlinkON(100);

            switch(req.getRequest()){
                case "setLogin":
                    try{
                        LoginSetting login = gson.fromJson(req.getData(), LoginSetting.class);
                        if (ValidString(login.Username) && ValidString(login.Password)){
                            config.setProperty("WebUsername", login.Username);
                            config.setProperty("WebPassword", login.Password);
                            if (SaveProperties(currentDir,"config.properties",config)){
                                resp = new SocketioResponse("success", "Login Setting");
                            } else throw new Exception("Failed to save properties");
                        } else throw new Exception("Invalid Username or Password");
                    } catch (JsonSyntaxException e){
                        resp = new SocketioResponse("error", "Invalid JSON");
                    } catch (Exception e){
                        resp = new SocketioResponse("error", e.getMessage());
                    }
                    break;
                case "getLogin":
                    resp = SocketioResponse.fromLoginSetting("success", new LoginSetting(config.getProperty("WebUsername"), config.getProperty("WebPassword")));
                    break;
                case "setSipSetting":
                    try{
                        SipSetting sip = gson.fromJson(req.getData(), SipSetting.class);
                        if (ValidString(sip.Server) && ValidString(sip.Username) && ValidString(sip.Password)){
                            config.setProperty("SipServer", sip.Server);
                            config.setProperty("SipPort", String.valueOf(sip.Port));
                            config.setProperty("SipUsername", sip.Username);
                            config.setProperty("SipPassword", sip.Password);
                            if (SaveProperties(currentDir,"config.properties",config)){
                                resp = new SocketioResponse("success", "SIP Setting");
                            } else throw new Exception("Failed to save properties");
                        } else throw new Exception("Invalid SIP Setting");
                    } catch (JsonSyntaxException e){
                        resp = new SocketioResponse("error", "Invalid JSON");
                    } catch (Exception e){
                        resp = new SocketioResponse("error", e.getMessage());
                    }
                    break;
                case "getSipSetting":
                    resp =  SocketioResponse.fromSipSetting("success", new SipSetting(config.getProperty("SipServer"), Integer.parseInt(config.getProperty("SipPort")), config.getProperty("SipUsername"), config.getProperty("SipPassword")));
                    break;
                case "getSipStatus":
                    resp = new SocketioResponse("success", SipStatus);
                    break;
                case "hangup":
                    if (incomingRequest!=null){
                        resp = new SocketioResponse("success", "Reject Incoming Call from "+incomingRequest.CallID);
                        hangupCall();
                    } else if (oncallRequest!=null){
                        resp = new SocketioResponse("success", "Hangup Call to "+oncallRequest.CallID);
                        hangupCall();
                    } else {
                        resp = new SocketioResponse("error", "No Call to Hangup");
                    }
                    break;
                case "getRamInfo":
                    if (ramInformation!=null){
                        resp = SocketioResponse.fromRamInformation("success", ramInformation);
                    } else {
                        resp = new SocketioResponse("error", "Failed to get RAM Information");
                    }
                    break;
                case "getCpuInfo":
                    if (cpuTemperature>0 && !cpuUsage.isEmpty()){
                        resp = SocketioResponse.fromCpuStatus("success", cpuTemperature, cpuUsage);
                    } else {
                        resp = new SocketioResponse("error", "Failed to get CPU Information");
                    }
                    break;
                case "getNetworkInfo":
                    if (!networkTX.isEmpty() && !networkRX.isEmpty()){
                        resp = SocketioResponse.fromTxRxMap("success", networkTX, networkRX);
                    } else {
                        resp = new SocketioResponse("error", "Failed to get Network Information");
                    }
                    break;
                case "call":
                case "getDiskInfo":
                    resp = new SocketioResponse("error", "Not Implemented");
                    break;
                default:
                    resp = new SocketioResponse("error", "Invalid Request");
                    break;
            }
            return resp;
        });

        // GPIO Section
        // talk button = pin12
        // hangup button = pin14
        // Call light = pin 22
        // Network light = pin 20
        callButton = new GpioInput(NanopiDuo2.Pin12.gpionumber, true);
        if (callButton.isInitialized()){
            callButton.setOnLongPress(vv->{
                Logger.info("Call Button Long Pressed");
                pickupCall();
            });
            callButton.setOnShortPress(vv->{
                Logger.info("Call Button Short Pressed");
                pickupCall();
            });
        }
        hangupButton = new GpioInput(NanopiDuo2.Pin14.gpionumber, true);
        if (hangupButton.isInitialized()){
            hangupButton.setOnShortPress(vv->{
                Logger.info("Hangup Button Short Pressed");
                hangupCall();
            });
            hangupButton.setOnLongPress(vv->{
                Logger.info("Hangup Button Long Pressed");
                hangupCall();
            });
        }

        // belum ada pinout nya di PCB demo (04/12/2024)
        pilotLight = new GpioOutput(NanopiDuo2.Pin16.gpionumber, true);
        if (pilotLight.isInitialized()){
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    if (pilotLight!=null && pilotLight.isInitialized()) pilotLight.BlinkON(100);
                }
            }, 0, 1000);
        }

        networkLight = new GpioOutput(NanopiDuo2.Pin20.gpionumber, true);
        callLight = new GpioOutput(NanopiDuo2.Pin22.gpionumber, true);

        // belum ada pinout nya di PCB demo (04/12/2024)
        Buzzer = new GpioOutput(NanopiDuo2.Pin18.gpionumber, true);

        // Start System monitoring
        init_system_monitoring();

        // Shutdown Hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            Logger.info("Shutting down SIPIntercom");
            if (client!=null) client.Disconnect();
            if (webserver!=null) webserver.Stop();
            if (socketioserver!=null) socketioserver.Stop();
            if (callButton!=null && callButton.isInitialized()) callButton.Close();
            if (hangupButton!=null && hangupButton.isInitialized()) hangupButton.Close();
            if (pilotLight!=null && pilotLight.isInitialized()) pilotLight.Close();
            if (networkLight!=null && networkLight.isInitialized()) networkLight.Close();
            if (callLight!=null && callLight.isInitialized()) callLight.Close();
            if (timer!=null) timer.cancel();
            if (system_monitoring_timer!=null) system_monitoring_timer.cancel();
        }));
    }

    /**
     * Hangup call
     */
    private static void hangupCall() {
        if (oncallRequest!=null || oncallResponse!=null){
            client.HangUp();
            oncallResponse = null;
            oncallRequest = null;
        } else if (incomingRequest!=null || incomingResponse!=null){
            client.RejectIncomingCall(incomingRequest);
            incomingRequest = null;
            incomingResponse = null;
        }
        callLight_Idle();
        Buzzer_Off();
        SipStatus = "Idle";
    }

    /**
     * Pickup incoming call
     */
    private static void pickupCall() {
        if (incomingRequest!=null && incomingResponse!=null){
            client.AcceptIncomingCall(incomingRequest);
            callLight_OnCall();
            Buzzer_OnCall();
            oncallRequest = incomingRequest;
            oncallResponse = incomingResponse;
            incomingRequest = null;
            incomingResponse = null;
            SipStatus = "Communication with "+oncallRequest.CallID;
        }
    }
    private static String CreateSipCallNumber(String extension){
        return MessageFormat.format("sip:{0}@{1}", extension, client.getServerAddress());
    }

    @SuppressWarnings("unused")
    private static void CallTest(String extension){
        String callnumber = CreateSipCallNumber(extension);
        if (client.Call(callnumber)){
            Logger.info("Call to {} is successful",callnumber);
            try{
                Thread.sleep(30*1000);
            } catch (InterruptedException ignored) {
            }
            client.HangUp();
        }
        else{
            Logger.error("Call to {} is failed",callnumber);
        }
    }

    @SuppressWarnings("unused")
    private static void StreamTest(String extension, String filename){
        File ff = new File(currentDir, filename);
        if (ff.isFile()){
            String callnumber = MessageFormat.format("sip:{0}@{1}", extension, client.getServerAddress());
            if (client.StreamFile(callnumber, ff.getAbsolutePath())){
                Logger.info("Stream to {} is successful",callnumber);
                try{
                    Thread.sleep(30*1000);
                } catch (InterruptedException ignored) {
                }
                client.HangUp();
            }
            else{
                Logger.error("Stream to {} is failed",callnumber);
            }
        } else Logger.error("File not found: {}",ff.getAbsolutePath());
    }

    /**
     * Registering state, call light is blinking with 500ms interval
     */
    private static void callLight_Registering(){
        if (callLightTask!=null) {
            callLightTask.cancel();
            callLightTask = null;
        }
        if (timer!=null){
            timer.purge();
            callLightTask = CreateCallLightTask();
            timer.scheduleAtFixedRate(callLightTask, 0, 500);
        }
    }

    /**
     * Idle state, call light is blinking with interval 3000ms
     */
    private static void callLight_Idle(){
        if (callLightTask!=null) {
            callLightTask.cancel();
            callLightTask = null;
        }
        if (timer!=null){
            timer.purge();
            callLightTask = CreateCallLightTask();
            timer.scheduleAtFixedRate(callLightTask, 0, 3000);
        }
    }

    /**
     * Incoming call, call light is blinking with 1000ms interval
     */
    private static void callLight_IncomingCall(){
        if (callLightTask!=null) {
            callLightTask.cancel();
            callLightTask = null;
        }
        if (timer!=null){
            timer.purge();
            callLightTask = CreateCallLightTask();
            timer.scheduleAtFixedRate(callLightTask, 0, 1000);
        }
    }

    /**
     * On call, call light is on
     */
    private static void callLight_OnCall(){
        if (callLightTask!=null) {
            callLightTask.cancel();
            callLightTask = null;
        }
        if (timer!=null) timer.purge();
        callLight.SetValue(true);
    }

    /**
     * Incoming call, buzzer is beeping with 1000ms interval
     */
    private static void Buzzer_IncomingCall(){
        if (BuzzerTask!=null) {
            BuzzerTask.cancel();
            BuzzerTask = null;
        }
        if (timer!=null){
            timer.purge();
            BuzzerTask = CreateBuzzerTask();
            timer.scheduleAtFixedRate(BuzzerTask, 0, 1000);
        }
    }

    /**
     * On-call, buzzer is off
     */
    private static void Buzzer_OnCall(){
        if (BuzzerTask!=null) {
            BuzzerTask.cancel();
            BuzzerTask = null;
        }
        if (timer!=null) timer.purge();
        if (Buzzer!=null) Buzzer.SetValue(false);
    }

    private static void Buzzer_Off(){
        if (BuzzerTask!=null) {
            BuzzerTask.cancel();
            BuzzerTask = null;
        }
        if (timer!=null) timer.purge();
        if (Buzzer!=null) Buzzer.SetValue(false);
    }

    private static TimerTask CreateBuzzerTask(){
        return new TimerTask() {
            @Override
            public void run() {
                if (Buzzer!=null && Buzzer.isInitialized()) Buzzer.BlinkON(500);
            }
        };
    }

    private static TimerTask CreateCallLightTask(){
        return new TimerTask() {
            @Override
            public void run() {
                if (callLight!=null && callLight.isInitialized()) callLight.BlinkON(100);
            }
        };
    }

    private static void init_system_monitoring(){
        TimerTask tt = new TimerTask() {
            @Override
            public void run() {
                cpuTemperature = SystemInformation.getCPUTemperature();
                ramInformation = SystemInformation.getRAMInformation();
                ProcessorStatus[] cpuinfo = SystemInformation.getProcStat();
                if (cpuinfo.length>0){
                    if (previousCpuInfo==null || !Objects.equals(previousCpuInfo.length, cpuinfo.length)){
                        previousCpuInfo = cpuinfo;
                    } else {
                        for(int ii=0;ii<previousCpuInfo.length;ii++){
                            cpuUsage.put(cpuinfo[ii].name, cpuinfo[ii].cpu_usage(previousCpuInfo[ii]));
                        }
                        previousCpuInfo = cpuinfo;
                    }
                }
                NetworkTransmitReceiveInfo[] ntri = SystemInformation.getNetworkTransmitReceiveInfo();
                if (ntri.length>0){
                    if (previousNetworkInfo==null || !Objects.equals(previousNetworkInfo.length, ntri.length)){
                        previousNetworkInfo = ntri;
                    } else {
                        for(int ii=0;ii<previousNetworkInfo.length;ii++){
                            double txspeed = ntri[ii].TxSpeed(previousNetworkInfo[ii], "B");
                            String txspeedstr;
                            if (txspeed < KB_threshold){
                                txspeedstr = String.format("%.0f B/s", txspeed);
                            } else if (txspeed < MB_threshold){
                                txspeedstr = String.format("%.1f KB/s", ntri[ii].TxSpeed(previousNetworkInfo[ii], "KB"));
                            } else if (txspeed < GB_threshold){
                                txspeedstr = String.format("%.1f MB/s", ntri[ii].TxSpeed(previousNetworkInfo[ii], "MB"));
                            } else {
                                txspeedstr = String.format("%.1f GB/s", ntri[ii].TxSpeed(previousNetworkInfo[ii], "GB"));
                            }
                            double rxspeed = ntri[ii].RxSpeed(previousNetworkInfo[ii], "B");
                            String rxspeedstr;
                            if (rxspeed < KB_threshold){
                                rxspeedstr = String.format("%.0f B/s", rxspeed);
                            } else if (rxspeed < MB_threshold){
                                rxspeedstr = String.format("%.1f KB/s", ntri[ii].RxSpeed(previousNetworkInfo[ii], "KB"));
                            } else if (rxspeed < GB_threshold){
                                rxspeedstr = String.format("%.1f MB/s", ntri[ii].RxSpeed(previousNetworkInfo[ii], "MB"));
                            } else {
                                rxspeedstr = String.format("%.1f GB/s", ntri[ii].RxSpeed(previousNetworkInfo[ii], "GB"));
                            }
                            networkTX.put(ntri[ii].name, txspeedstr);
                            networkRX.put(ntri[ii].name, rxspeedstr);
                        }
                        previousNetworkInfo = ntri;
                    }
                }
            }
        };
        system_monitoring_timer = new Timer();
        system_monitoring_timer.scheduleAtFixedRate(tt, 1000, 5000);

    }

}