// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com


package engine.job;

import engine.core.ControlledRunnable;
import engine.server.MBServerStatics;
import engine.util.ThreadUtils;
import org.pmw.tinylog.Logger;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;


/**
 * Note to DEVs. When attempting to log something in this class, use logDIRECT
 * only.
 */
public class JobManager extends ControlledRunnable {

    /*
     * Singleton implementation.
     */
    private static volatile JobManager INSTANCE;
    private final ArrayList<JobPool> jobPoolList = new ArrayList<>();

    /*
     * Class implementation
     */
    private final ConcurrentHashMap<String, JobPool> jobQueueMapping = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_HIGH);
    private boolean shutdownNowFlag = false;

    private JobManager() {
        super("JobManager");

        // create the initial job pools with the correct sizes
        // based on the number of array elements in the initial_jo_workers
        // definition in Statisc

        if (MBServerStatics.INITIAL_JOBPOOL_WORKERS != null && MBServerStatics.INITIAL_JOBPOOL_WORKERS.length > 0) {
            for (int i = 0; i < MBServerStatics.INITIAL_JOBPOOL_WORKERS.length; i++) {
                JobPool jp = new JobPool(i, MBServerStatics.INITIAL_JOBPOOL_WORKERS[i]);
                this.jobPoolList.add(jp);
                Logger.info("Adding JobPool_" + jp.getJobPoolID() + " with " + MBServerStatics.INITIAL_JOBPOOL_WORKERS[i] + " workers");
            }
        } else {
            // not defined or empty in statics so just create 1 JobPool with 25 workers
            System.out.println("Creating 1 pool called 0");
            JobPool jp = new JobPool(0, 25);
            this.jobPoolList.add(jp);
            Logger.info("Adding JobPool_" + jp.getJobPoolID() + " with 25 workers");
        }

        JobScheduler.getInstance();

        // if you want any jobs to default onto a given queue put it here
        // otherwise everything goes on the P1 queue by default

    }

    public static JobManager getInstance() {
        if (JobManager.INSTANCE == null) {
            synchronized (JobManager.class) {
                if (JobManager.INSTANCE == null) {
                    JobManager.INSTANCE = new JobManager();
                    JobManager.INSTANCE.startup();
                }
            }
        }
        return JobManager.INSTANCE;
    }

    /**
     * Submit a job to be processed by the JobManager. There is no guarantee
     * that the job will be executed immediately.
     *
     * @param aj AbstractJob to be submitted.
     * @return boolean value indicating whether the job was submitted or not.
     */
    public boolean submitJob(AbstractJob aj) {

        if (jobQueueMapping.containsKey(aj.getClass().getSimpleName())) {
            return this.jobQueueMapping.get(aj.getClass().getSimpleName()).submitJob(aj);
        } else {
            return this.jobPoolList.get(0).submitJob(aj);
        }
    }


    private void auditAllWorkers() {

        for (JobPool jp : this.jobPoolList) {
            jp.auditWorkers();
        }
    }

    @Override
    protected boolean _Run() {
        // This thread is to be used to JM internal monitoring.

        // Startup workers:
        this.auditAllWorkers();
        ThreadUtils.sleep(MBServerStatics.JOBMANAGER_INTERNAL_MONITORING_INTERVAL_MS * 2);

        this.runStatus = true;

        // Monitoring loop
        while (this.runCmd) {
            this.auditAllWorkers();
            ThreadUtils.sleep(MBServerStatics.JOBMANAGER_INTERNAL_MONITORING_INTERVAL_MS);
        }

        // No new submissions
        for (JobPool jp : this.jobPoolList) {
            jp.setBlockNewSubmissions(true);
        }

        if (this.shutdownNowFlag == false) {
            // shutdown each pool
            for (JobPool jp : this.jobPoolList) {
                jp.shutdown();
            }
        } else {
            // Emergency Stop each pool
            for (JobPool jp : this.jobPoolList) {
                jp.emergencyStop();
            }
        }
        this.runStatus = false;
        Logger.info("JM Shutdown");
        return true;
    }

    @Override
    protected boolean _postRun() {
        return true;
    }

    @Override
    protected boolean _preRun() {
        return true;
    }

    @Override
    protected void _shutdown() {
        Logger.info("JM Shutdown Requested");
        JobScheduler.getInstance().shutdown();
    }

    @Override
    protected void _startup() {
        Logger.info("JM Starting up... ");
    }

    public void shutdownNow() {
        this.shutdownNowFlag = true;
        this.shutdown();
    }

    public String moveJob(String simpleClassName, String jobPoolID) {
        // moves a job to a different queue

        try {
            // parse string into an int
            Integer i = Integer.parseInt(jobPoolID);
            for (JobPool jp : this.jobPoolList) {
                if (jp.getJobPoolID() == i) {
                    // move the job to the new queue
                    this.jobQueueMapping.put(simpleClassName, jp);
                    return simpleClassName + " moved to JobPool ID " + jp.getJobPoolID();
                }
            }

        } catch (NumberFormatException e) {
            return "jobPoolID is not a valid number";
        }

        //Verify jobpool ID
        return "Invalid parameters <simpleClassName - case sensitive> <jobPoolID> required";
    }

    public String resetJobs() {
        // moves all jobs to the P1 queue
        this.jobQueueMapping.clear();
        return "All Jobs reset onto P1 queue";
    }

    public String showJobs() {
        String out = "";
        Iterator<String> jmi = this.jobQueueMapping.keySet().iterator();

        while (jmi.hasNext()) {
            String jmiKey = jmi.next();
            out += jmiKey + ' ' + this.jobQueueMapping.get(jmiKey).getJobPoolID() + '\n';
        }
        return out;
    }

    public ArrayList<JobPool> getJobPoolList() {

        return this.jobPoolList;
    }

    public String modifyJobPoolWorkers(String jobPoolID, String maxWorkers) {

        try {
            // parse string into an int
            Integer jid = Integer.parseInt(jobPoolID);
            Integer mw = Integer.parseInt(maxWorkers);
            for (JobPool jp : this.jobPoolList) {
                if (jp.getJobPoolID() == jid) {
                    // change the number of workers
                    return jp.setMaxWorkers(mw);
                }
            }

        } catch (NumberFormatException e) {
            return "Invalid parameters <jobPoolID> <maxWorkers> required";
        }

        return "Invalid parameters <jobPoolID> <maxWorkers> required";
    }
}
