/*
 * Copyright 2018 Anton Tananaev (anton@traccar.org)
 *
 * 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.
 */
package org.traccar;

import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Advapi32;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.Winsvc;
import com.sun.jna.platform.win32.Winsvc.HandlerEx;
import com.sun.jna.platform.win32.Winsvc.SC_HANDLE;
import com.sun.jna.platform.win32.Winsvc.SERVICE_DESCRIPTION;
import com.sun.jna.platform.win32.Winsvc.SERVICE_MAIN_FUNCTION;
import com.sun.jna.platform.win32.Winsvc.SERVICE_STATUS;
import com.sun.jna.platform.win32.Winsvc.SERVICE_STATUS_HANDLE;
import com.sun.jna.platform.win32.Winsvc.SERVICE_TABLE_ENTRY;
import jnr.posix.POSIXFactory;

import java.io.File;
import java.net.URISyntaxException;

public abstract class WindowsService {

    private static final Advapi32 ADVAPI_32 = Advapi32.INSTANCE;

    private final Object waitObject = new Object();

    private String serviceName;
    private ServiceMain serviceMain;
    private ServiceControl serviceControl;
    private SERVICE_STATUS_HANDLE serviceStatusHandle;

    public WindowsService(String serviceName) {
        this.serviceName = serviceName;
    }

    public boolean install(
            String displayName, String description, String[] dependencies,
            String account, String password, String config) throws URISyntaxException {

        String javaHome = System.getProperty("java.home");
        String javaBinary = javaHome + "\\bin\\java.exe";

        File jar = new File(WindowsService.class.getProtectionDomain().getCodeSource().getLocation().toURI());
        String command = javaBinary
                + " -Duser.dir=\"" + jar.getParentFile().getAbsolutePath() + "\""
                + " -jar \"" + jar.getAbsolutePath() + "\""
                + " --service \"" + config + "\"";

        boolean success = false;
        StringBuilder dep = new StringBuilder();

        if (dependencies != null) {
            for (String s : dependencies) {
                dep.append(s);
                dep.append("\0");
            }
        }
        dep.append("\0");

        SERVICE_DESCRIPTION desc = new SERVICE_DESCRIPTION();
        desc.lpDescription = description;

        SC_HANDLE serviceManager = openServiceControlManager(null, Winsvc.SC_MANAGER_ALL_ACCESS);

        if (serviceManager != null) {
            SC_HANDLE service = ADVAPI_32.CreateService(serviceManager, serviceName, displayName,
                    Winsvc.SERVICE_ALL_ACCESS, WinNT.SERVICE_WIN32_OWN_PROCESS, WinNT.SERVICE_AUTO_START,
                    WinNT.SERVICE_ERROR_NORMAL,
                    command,
                    null, null, dep.toString(), account, password);

            if (service != null) {
                success = ADVAPI_32.ChangeServiceConfig2(service, Winsvc.SERVICE_CONFIG_DESCRIPTION, desc);
                ADVAPI_32.CloseServiceHandle(service);
            }
            ADVAPI_32.CloseServiceHandle(serviceManager);
        }
        return success;
    }

    public boolean uninstall() {
        boolean success = false;

        SC_HANDLE serviceManager = openServiceControlManager(null, Winsvc.SC_MANAGER_ALL_ACCESS);

        if (serviceManager != null) {
            SC_HANDLE service = ADVAPI_32.OpenService(serviceManager, serviceName, Winsvc.SERVICE_ALL_ACCESS);

            if (service != null) {
                success = ADVAPI_32.DeleteService(service);
                ADVAPI_32.CloseServiceHandle(service);
            }
            ADVAPI_32.CloseServiceHandle(serviceManager);
        }
        return success;
    }

    public boolean start() {
        boolean success = false;

        SC_HANDLE serviceManager = openServiceControlManager(null, WinNT.GENERIC_EXECUTE);

        if (serviceManager != null) {
            SC_HANDLE service = ADVAPI_32.OpenService(serviceManager, serviceName, WinNT.GENERIC_EXECUTE);

            if (service != null) {
                success = ADVAPI_32.StartService(service, 0, null);
                ADVAPI_32.CloseServiceHandle(service);
            }
            ADVAPI_32.CloseServiceHandle(serviceManager);
        }

        return success;
    }

    public boolean stop() {
        boolean success = false;

        SC_HANDLE serviceManager = openServiceControlManager(null, WinNT.GENERIC_EXECUTE);

        if (serviceManager != null) {
            SC_HANDLE service = Advapi32.INSTANCE.OpenService(serviceManager, serviceName, WinNT.GENERIC_EXECUTE);

            if (service != null) {
                SERVICE_STATUS serviceStatus = new SERVICE_STATUS();
                success = Advapi32.INSTANCE.ControlService(service, Winsvc.SERVICE_CONTROL_STOP, serviceStatus);
                Advapi32.INSTANCE.CloseServiceHandle(service);
            }
            Advapi32.INSTANCE.CloseServiceHandle(serviceManager);
        }

        return success;
    }

    public void init() throws URISyntaxException {
        String path = new File(
                WindowsService.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();

        POSIXFactory.getPOSIX().chdir(path);

        serviceMain = new ServiceMain();
        SERVICE_TABLE_ENTRY entry = new SERVICE_TABLE_ENTRY();
        entry.lpServiceName = serviceName;
        entry.lpServiceProc = serviceMain;

        Advapi32.INSTANCE.StartServiceCtrlDispatcher((SERVICE_TABLE_ENTRY[]) entry.toArray(2));
    }

    private SC_HANDLE openServiceControlManager(String machine, int access) {
        return ADVAPI_32.OpenSCManager(machine, null, access);
    }

    private void reportStatus(int status, int win32ExitCode, int waitHint) {
        SERVICE_STATUS serviceStatus = new SERVICE_STATUS();
        serviceStatus.dwServiceType = WinNT.SERVICE_WIN32_OWN_PROCESS;
        serviceStatus.dwControlsAccepted = Winsvc.SERVICE_ACCEPT_STOP | Winsvc.SERVICE_ACCEPT_SHUTDOWN;
        serviceStatus.dwWin32ExitCode = win32ExitCode;
        serviceStatus.dwWaitHint = waitHint;
        serviceStatus.dwCurrentState = status;

        ADVAPI_32.SetServiceStatus(serviceStatusHandle, serviceStatus);
    }

    public abstract void run();

    private class ServiceMain implements SERVICE_MAIN_FUNCTION {

        public void callback(int dwArgc, Pointer lpszArgv) {
            serviceControl = new ServiceControl();
            serviceStatusHandle = ADVAPI_32.RegisterServiceCtrlHandlerEx(serviceName, serviceControl, null);

            reportStatus(Winsvc.SERVICE_START_PENDING, WinError.NO_ERROR, 3000);
            reportStatus(Winsvc.SERVICE_RUNNING, WinError.NO_ERROR, 0);

            Thread.currentThread().setContextClassLoader(WindowsService.class.getClassLoader());

            run();

            try {
                synchronized (waitObject) {
                    waitObject.wait();
                }
            } catch (InterruptedException ex) {
            }

            reportStatus(Winsvc.SERVICE_STOPPED, WinError.NO_ERROR, 0);

            // Avoid returning from ServiceMain, which will cause a crash
            // See http://support.microsoft.com/kb/201349, which recommends
            // having init() wait for this thread.
            // Waiting on this thread in init() won't fix the crash, though.

            System.exit(0);
        }

    }

    private class ServiceControl implements HandlerEx {

        public int callback(int dwControl, int dwEventType, Pointer lpEventData, Pointer lpContext) {
            switch (dwControl) {
                case Winsvc.SERVICE_CONTROL_STOP:
                case Winsvc.SERVICE_CONTROL_SHUTDOWN:
                    reportStatus(Winsvc.SERVICE_STOP_PENDING, WinError.NO_ERROR, 5000);
                    synchronized (waitObject) {
                        waitObject.notifyAll();
                    }
                    break;
                default:
                    break;
            }
            return WinError.NO_ERROR;
        }

    }

}