package id.co.gtc;

import Audio.AudioFileProperties;
import Audio.AudioPlayer;
import Audio.Bass;
import Audio.PlaybackEvent;
import Camera.PanTiltController;
import Camera.RtspGrabber;
import Camera.VapixProtocol;
import Other.SomeCodes;
import SBC.*;
import Web.*;
import com.google.gson.JsonObject;
import org.bytedeco.opencv.global.opencv_core;
import org.tinylog.Logger;

import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static Other.SomeCodes.*;

public class Main {
    private static AudioPlayer audioPlayer;
    private static WebServer webServer;
    private static RtspGrabber rtspGrabber;
    private static PanTiltController panTiltController;
    private static AudioFileProperties audioFileProperties;
    private static VapixProtocol vapixProtocol;
    private static Timer timer;
    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<>();
    private static RaspberryPi5BPins AmplifierPower = null;
    private static RaspberryPi5BPins LedWeb = null;
    private static RaspberryPi5BPins LedIpCamera = null;
    private static RaspberryPi5BPins LedPanTilt = null;
    private static RaspberryPi5BPins Max485Direction = null;

    private static ExecutorService gpioExecutor = null;

    // Application start from here
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (audioPlayer!=null) audioPlayer.Unload();
            if (webServer!=null) webServer.Stop();
            if (rtspGrabber!=null) rtspGrabber.Stop();
            if (panTiltController!=null) panTiltController.Close();
            if (vapixProtocol!=null) vapixProtocol.Close();
            if (timer!=null) timer.cancel();
            if (gpioExecutor!=null) gpioExecutor.shutdown();
            if (AmplifierPower!=null) {
                Logger.info("GPIO pin {} unexport : {}",AmplifierPower.pin, GPIO.UnexportPin(AmplifierPower));
            }
            if (LedWeb!=null) {
                Logger.info("GPIO pin {} unexport : {}", LedWeb.pin,GPIO.UnexportPin(LedWeb));
            }
            if (LedIpCamera!=null) {
                Logger.info("GPIO pin {} unexport : {}", LedIpCamera.pin, GPIO.UnexportPin(LedIpCamera));
            }
            if (LedPanTilt!=null) {
                Logger.info("GPIO pin {} unexport : {}", LedPanTilt.pin,GPIO.UnexportPin(LedPanTilt));
            }
            if (Max485Direction!=null) {
                Logger.info("GPIO pin {} unexport : {}", Max485Direction.pin, GPIO.UnexportPin(Max485Direction));
            }

        }));

        init_gpio();
        init_system_monitoring();

        init_properties();
        init_audiofiles();

        init_Vapix();
        init_audio();
        init_pantiltcontroller();
        init_webserver();
        init_rtspgrabber();
    }

    private static void init_gpio(){
        if (GPIO.HaveGPIO()){
            gpioExecutor = Executors.newVirtualThreadPerTaskExecutor();
            AmplifierPower = InitializePin(RaspberryPi5BPins.Pin13, true);
            LedWeb = InitializePin(RaspberryPi5BPins.Pin15, true);
            LedIpCamera = InitializePin(RaspberryPi5BPins.Pin19, true);
            LedPanTilt = InitializePin(RaspberryPi5BPins.Pin21, true);
            Max485Direction = InitializePin(RaspberryPi5BPins.Pin23, true);
        }
    }


    @SuppressWarnings("SameParameterValue")
    private static RaspberryPi5BPins InitializePin(RaspberryPi5BPins pin, boolean isOutput){
        boolean exported = false;
        if (GPIO.GpioPinExists(pin)){
            exported = true;
        } else {
            if (GPIO.ExportPin(pin)){
                exported = true;
            }
        }

        if (exported){
            if (GPIO.SetPinDirection(pin, isOutput?"out":"in")){
                return pin;
            }
        }
        return null;
    }

    // dipanggil di PanTiltController, maka perlu public dan static
    public static void Max485Direction(boolean isTransmit){
        if (Max485Direction!=null){
            GPIO.SetValue(Max485Direction, isTransmit);
        }
    }

    // dipanggil di PanTiltController, maka perlu public dan static
    public static void Blink_LedPanTilt(){
        Blink(LedPanTilt);
    }

    private static void AmplifierControl(boolean isON){
        if (AmplifierPower!=null){
            GPIO.SetValue(AmplifierPower, isON);
        }
    }

    private static void Blink(RaspberryPi5BPins pin){
        if (pin!=null){
            if (gpioExecutor!=null){
                gpioExecutor.submit(()->{
                    GPIO.SetValue(pin, true);
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException ignored) {
                    }
                    GPIO.SetValue(pin, false);
                });
            }
        }
    }

    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;
                    }
                }
            }
        };
        timer = new Timer();
        timer.scheduleAtFixedRate(tt, 1000, 5000);

    }

    private static void init_properties(){
        if (ExtractResource("/tinylog.properties", currentDirectory)==null) Logger.error("Failed to extract tinylog.properties");
        if (ExtractResource("/config.properties", currentDirectory)==null) Logger.error("Failed to extract config.properties");
        if (ExtractResource("/simplelogger.properties", currentDirectory)==null) Logger.error("Failed to extract simplelogger.properties");
    }

    private static void init_audiofiles(){
        if (audioPath.toFile().isDirectory()) {
            Logger.info("Audio Files Directory exists : "+audioPath);
        } else {
            if (audioPath.toFile().mkdirs())  Logger.info("Audio Files Directory created : "+audioPath);
        }
        if (ExtractResource("/elangWav.wav", audioPath.toString())==null) Logger.error("Failed to extract elangWav.wav");
        if (ExtractResource("/gunshotsWav.wav", audioPath.toString())==null) Logger.error("Failed to extract gunshotsWav.wav");
        if (ExtractResource("/pinkNoiseWav.wav", audioPath.toString())==null) Logger.error("Failed to extract pinkNoiseWav.wav");
    }

    private static void init_Vapix(){
        Properties config = SomeCodes.LoadProperties("config.properties");
        String ip = config.getProperty("Camera_ip");
        String port = config.getProperty("Camera_port");
        String username = config.getProperty("Camera_user");
        String password = config.getProperty("Camera_password");
        if (ValidString(ip)){
            if (ValidPortNumber(port)){
                if (ValidString(username)){
                    if (ValidString(password)){
                        if (IpIsReachable(ip)){
                            Logger.info("Camera IP is reachable");
                            vapixProtocol = new VapixProtocol(ip, Integer.parseInt(port), username, password);
                            if (vapixProtocol.isSuccessQuery()){
                                Logger.info("Camera Product Number: "+vapixProtocol.GetProductNumber());
                                Logger.info("Camera Serial Number: "+vapixProtocol.GetSerialNumber());
                                if (vapixProtocol.PTZEnabled()){
                                    Logger.info("PTZ Enabled");
                                } else Logger.error("PTZ Disabled");
                                Logger.info("Max Zoom: "+vapixProtocol.GetPTZMaxZoom());
                                Logger.info("Min Zoom: "+vapixProtocol.GetPTZMinZoom());
                                Blink(LedIpCamera);
                            } else Logger.error("Failed to query camera");
                        } else Logger.error("Camera IP is not reachable");
                    } else Logger.error("Camera Password is not valid string");
                } else Logger.error("Camera Username is not valid string");
            } else Logger.error("Camera Port is not valid");
        } else Logger.error( "Camera IP is not valid string");
    }

    private static void init_pantiltcontroller() {
        Properties config = SomeCodes.LoadProperties("config.properties");
        String portname = config.getProperty("SerialPort");
        String baudrate = config.getProperty("SerialBaudRate");
        String PanTiltID = config.getProperty("PanTiltID");
        if (ValidString(portname)){
            if (ValidInteger(baudrate)){
                if (ValidInteger(PanTiltID)){
                    panTiltController = new PanTiltController(portname, Integer.parseInt(baudrate), Integer.parseInt(PanTiltID));
                }
            } else Logger.error("Invalid PTZ Baudrate");
        } else Logger.error("Invalid PTZ Port");
    }

    private static void init_rtspgrabber() {
        if (haveOpenCL) opencv_core.setUseOpenCL(true);
        // check if really activated
        useOpenCL = opencv_core.useOpenCL();
        Logger.info("OpenCL available={}, activated={}", haveOpenCL, useOpenCL);
        Properties config = SomeCodes.LoadProperties("config.properties");
        String targetip = config.getProperty("Camera_ip");
        String rtsppath = config.getProperty("Camera_Rtsp_path");
        rtspGrabber = null;
        if (ValidString(targetip)){
            if (ValidString(rtsppath)){
                if (IpIsReachable(targetip)){
                    Logger.info("Camera IP is reachable");
                    rtspGrabber = new RtspGrabber(targetip, rtsppath);
                    rtspGrabber.Start(true, 1920, 1080);
                } else Logger.error("Camera IP is not reachable");
            } else Logger.error("Camera Path is not valid string");
        } else Logger.error("Camera IP is not valid string");
    }

    private static void init_audio() {
        audioPlayer = new AudioPlayer();
        audioPlayer.DetectOutputDevices();
        int devid = audioPlayer.FindDeviceIDWithName("USB");
        if (devid<1) devid = audioPlayer.FindDeviceIDWithName("Speakers");

        if (devid>0){
            Bass.BASS_DEVICEINFO info = audioPlayer.GetDeviceInfo(devid);
            if (audioPlayer.OpenDevice(devid,48000)){
                audioPlayer.setMasterVolume(100);
                audioPlayer.setPlaybackvolume(100);
                Logger.info("Audio Device ID={} opened : {}", devid, info.name);
            } else Logger.error("Failed to open Audio Device ID={}", devid);
        } else Logger.error("USB Audio Device not found");
    }

    static PlaybackEvent pe = new PlaybackEvent() {
        @Override
        public void onPlaybackStart(AudioFileProperties prop) {
            Logger.info("Playback started for {}", prop.filename);
        }

        @Override
        public void onPlaybackFinished(AudioFileProperties prop) {
            Logger.info("Playback finished for {}", prop.filename);
        }

        @Override
        public void onPlaybackFailure(AudioFileProperties prop, String reason) {
            Logger.error("Playback failed for {}: {}", prop.filename, reason);
        }

        @Override
        public void onPlaybackLooped(AudioFileProperties prop) {
            Logger.info("Playback looped for {}", prop.filename);
        }
    };

    static WebsocketEvent we = new WebsocketEvent() {
        @Override
        public String onMessage(String message) {
            return "";
        }

        @Override
        public WebsocketReply onWebsocketCommand(WebsocketCommand command) {
            Blink(LedWeb);
            byte speed;
            String cmd = command.command.toUpperCase().trim();
            switch (cmd){
                // Pan Tilt Movement Commands
                case "PAN LEFT" :
                    speed = GetPanTiltSpeed(command.data, (byte)0x20);
                    if (panTiltController!=null) panTiltController.PanLeft(speed);
                    return new WebsocketReply("PAN LEFT", String.valueOf(speed));
                case "PAN RIGHT" :
                    speed = GetPanTiltSpeed(command.data, (byte)0x20);
                    if (panTiltController!=null) panTiltController.PanRight(speed);
                    return new WebsocketReply("PAN RIGHT", String.valueOf(speed));
                case "TILT UP" :
                    speed = GetPanTiltSpeed(command.data, (byte)0x20);
                    if (panTiltController!=null) panTiltController.TiltUp(speed);
                    return new WebsocketReply("TILT UP", String.valueOf(speed));
                case "TILT DOWN" :
                    speed = GetPanTiltSpeed(command.data, (byte)0x20);
                    if (panTiltController!=null) panTiltController.TiltDown(speed);
                    return new WebsocketReply("TILT DOWN", String.valueOf(speed));
                case "STOP MOVEMENT" :
                    if (panTiltController!=null) panTiltController.StopMovement();
                    return new WebsocketReply("STOP MOVEMENT", "");
                // Audio Related Commands
                case "MUTE":
                    audioPlayer.Mute();
                    return new WebsocketReply("MUTE", "");
                case "UNMUTE":
                    audioPlayer.Unmute();
                    return new WebsocketReply("UNMUTE", "");
                case "SET VOLUME" :
                    int volume=-1;
                    if (ValidInteger(command.data)){
                        volume = Integer.parseInt(command.data);
                        if (volume<0) volume = 0;
                        if (volume>100) volume = 100;
                    }
                    if (volume>=0){
                        audioPlayer.setPlaybackvolume(volume);
                        return new WebsocketReply("SET VOLUME", String.valueOf(volume));
                    } else return new WebsocketReply("SET VOLUME", "Invalid Volume Value");
                case "GET VOLUME" :
                    int vol = audioPlayer.getPlaybackvolume();
                    return new WebsocketReply("GET VOLUME", String.valueOf(vol));
                case "PLAY AUDIO" :
                    if (ValidInteger(command.data)){
                        int id = Integer.parseInt(command.data);
                        String filename = GetConfigAudioFile(id);
                        if (ValidString(filename)){
                            File filetoplay = new File(Path.of(currentDirectory, "audiofiles", filename).toString());
                            if (filetoplay.isFile()){
                                AudioFileProperties afp = audioPlayer.OpenAudioFile(filetoplay);
                                if (afp!=null){
                                    audioFileProperties = afp;
                                    audioPlayer.PlayAudioFile(afp, true, pe);
                                    AmplifierControl(true);
                                    return new WebsocketReply("PLAY AUDIO", afp.filename);

                                } else return new WebsocketReply("PLAY AUDIO", "Failed to open audio file "+filename);
                            } else return new WebsocketReply("PLAY AUDIO", "Audio file not found : "+filename);
                        } else return new WebsocketReply("PLAY AUDIO", String.format("AudioFile with ID %02d not found", id));
                    } else return new WebsocketReply("PLAY AUDIO", "Invalid Audio ID");
                case "STOP AUDIO" :
                    if (audioFileProperties!=null){
                        audioPlayer.CloseAudioFile(audioFileProperties);    // close previous audio file
                        String filename = audioFileProperties.filename;
                        audioFileProperties = null;
                        AmplifierControl(false);
                        return   new WebsocketReply("STOP AUDIO", filename);
                    } else return new WebsocketReply("STOP AUDIO", "No audioFileProperties");
                    // ZOOM Related Commands
                case "GET MAX ZOOM":
                    if (vapixProtocol!=null){
                        return new WebsocketReply("GET MAX ZOOM", String.valueOf(vapixProtocol.GetPTZMaxZoom()));
                    } else return new WebsocketReply("GET MAX ZOOM", "VapixProtocol not initialized");
                case "GET ZOOM":
                    if (vapixProtocol!=null){
                        return new WebsocketReply("GET ZOOM", String.valueOf(vapixProtocol.GetCurrentZoomValue()));
                    } else return new WebsocketReply("GET ZOOM", "VapixProtocol not initialized");
                case "SET ZOOM":
                    if (vapixProtocol.PTZEnabled()){
                        if (ValidInteger(command.data)){
                            int zoom = Integer.parseInt(command.data);
                            if (zoom<vapixProtocol.GetPTZMinZoom()) zoom = vapixProtocol.GetPTZMinZoom();
                            if (zoom>vapixProtocol.GetPTZMaxZoom()) zoom = vapixProtocol.GetPTZMaxZoom();
                            if (vapixProtocol.Zoom(1, zoom)){
                                Blink(LedIpCamera);
                                return new WebsocketReply("ZOOM", String.valueOf(zoom));
                            } else return new WebsocketReply("ZOOM", "Failed to zoom");
                        } else return new WebsocketReply("ZOOM", "Invalid Zoom Value");
                    } else return new WebsocketReply("ZOOM", "Zoom not supported");
                    // Live Streaming Related Commands
                case  "GET BASE64":
                    if (rtspGrabber!=null){
                        if (Objects.equals(command.data,"HQ"))
                            return new WebsocketReply("GET BASE64", "data:image/jpeg;base64,"+ rtspGrabber.getLastHQBase64(), rtspGrabber.HQStreamingStatus());
                        else
                            return new WebsocketReply("GET BASE64", "data:image/jpeg;base64,"+ rtspGrabber.getLastLQBase64(), rtspGrabber.LQStreamingStatus());
                    } else return new WebsocketReply("GET BASE64", "RTSP Grabber not initialized");
                case "GET RESOLUTION":
                    if (vapixProtocol!=null){
                        int[] res = vapixProtocol.GetCurrentResolution(1);
                        return new WebsocketReply("GET RESOLUTION", String.format("%dx%d", res[0], res[1]));
                    } else return new WebsocketReply("GET RESOLUTION", "VapixProtocol not initialized");
                case "GET AUDIOFILES":
                    AudioFilesInfo afi = new AudioFilesInfo();
                    afi.preset1 = GetConfigAudioFile(1);
                    afi.preset2 = GetConfigAudioFile(2);
                    afi.preset3 = GetConfigAudioFile(3);
                    afi.preset4 = GetConfigAudioFile(4);
                    afi.preset5 = GetConfigAudioFile(5);
                    return new WebsocketReply("GET AUDIOFILES", gson.toJson(afi));
                case "GET SYSTEM INFO":
                    JsonObject data = new JsonObject();
                    data.addProperty("cpu_temperature", String.valueOf(cpuTemperature));
                    if (ramInformation!=null) {
                        data.addProperty("ram_usage", String.format("%.1f", ramInformation.RamUsagePercentage()));
                    }
                    if (!cpuUsage.isEmpty()) {
                        for(String key : cpuUsage.keySet()){
                            data.addProperty(key, String.valueOf(cpuUsage.get(key)));
                        }
                    }
                    if (!networkTX.isEmpty()) {
                        for(String key : networkTX.keySet()){
                            if (key.equals("lo")) continue;
                            data.addProperty(key+"_TX", networkTX.get(key));
                        }
                    }
                    if (!networkRX.isEmpty()) {
                        for(String key : networkRX.keySet()){
                            if (key.equals("lo")) continue;
                            data.addProperty(key+"_RX", networkRX.get(key));
                        }
                    }
                    return new WebsocketReply("GET SYSTEM INFO", data.toString());
                default:
                    return new WebsocketReply("UNKNOWN COMMAND", command.command);
            }
        }

        @Override
        public void NewCameraConfiguration() {
            Logger.info("New Camera Configuration detected");
            Properties prop = SomeCodes.LoadProperties("config.properties");
            String camera_ip = prop.getProperty("Camera_ip");
            String camera_port = prop.getProperty("Camera_port");
            String camera_user = prop.getProperty("Camera_user");
            String camera_password = prop.getProperty("Camera_password");
            String camera_rtsp_path = prop.getProperty("Camera_Rtsp_path");
            Logger.info("Camera Ip: "+camera_ip);
            Logger.info("Camera Port: "+camera_port);
            Logger.info("Camera User: "+camera_user);
            Logger.info("Camera Password: "+camera_password);
            Logger.info("Camera Rtsp Path: "+camera_rtsp_path);

            if (rtspGrabber!=null) rtspGrabber.Stop();
            init_rtspgrabber();
            Logger.info("RtspGrabber recreated");

            if (vapixProtocol!=null) vapixProtocol.Close();
            init_Vapix();
            Logger.info("VapixProtocol recreated");

        }
    };

    private static void init_webserver() {
        Properties config = SomeCodes.LoadProperties("config.properties");
        String webusername = config.getProperty("WebUsername", "admin");
        String webpassword = config.getProperty("WebPassword", "bandara");
        String webhost = config.getProperty("WebHost","0.0.0.0");
        String webport = config.getProperty("WebPort","8080");

        webServer = new WebServer(we, webusername, webpassword);
        webServer.Start(webhost, Integer.parseInt(webport));
    }
}