package tech.andrey.jenkins.missioncontrol;

import hudson.Extension;
import hudson.model.*;
import hudson.security.Permission;
import hudson.util.RunList;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

import javax.servlet.ServletException;
import java.io.IOException;
import java.io.Serializable;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

@SuppressWarnings("unused")
@ExportedBean
public class MissionControlView extends View {
    private transient int getBuildsLimit;

    private int fontSize;

    private int buildQueueSize;

    private int buildHistorySize;

    private boolean useCondensedTables;

    private boolean filterByFailures;

    private boolean hideBuildHistory;

    private boolean hideJobs;

    private boolean hideBuildQueue;

    private boolean hideNodes;

    private String statusButtonSize;

    private String layoutHeightRatio;

    private String filterBuildHistory;

    private String filterJobStatuses;

    @DataBoundConstructor
    public MissionControlView(String name) {
        super(name);
        this.fontSize = 16;
        this.buildQueueSize = 10;
        this.buildHistorySize = 16;
        this.useCondensedTables = false;
        this.filterByFailures = false;
        this.hideBuildHistory = false;
        this.hideJobs = false;
        this.hideBuildQueue = false;
        this.hideNodes = false;
        this.statusButtonSize = "";
        this.layoutHeightRatio = "6040";
        this.filterBuildHistory = null;
        this.filterJobStatuses = null;
    }

    protected Object readResolve() {
        if (getBuildsLimit == 0)
            getBuildsLimit = 250;

        if (fontSize == 0)
            fontSize = 16;

        if (buildHistorySize == 0)
            buildHistorySize = 16;

        if (buildQueueSize == 0)
            buildQueueSize = 10;

        if (statusButtonSize == null)
            statusButtonSize = "";

        if (layoutHeightRatio == null)
            layoutHeightRatio = "6040";

        return this;
    }

    @Override
    public Collection<TopLevelItem> getItems() {
        return new ArrayList<TopLevelItem>();
    }

    public int getFontSize() {
        return fontSize;
    }

    public int getBuildHistorySize() {
        return buildHistorySize;
    }

    public int getBuildQueueSize() {
        return buildQueueSize;
    }

    public boolean isUseCondensedTables() {
        return useCondensedTables;
    }

    public boolean isFilterByFailures() {
        return filterByFailures;
    }

    public String getTableStyle() {
        return useCondensedTables ? "table-condensed" : "";
    }

    public String getBuildHistoryHeight() {
        if (hideBuildHistory)
            return "0";
        if (hideBuildQueue)
            return "100";
        return getTopHalfHeight();
    }

    public boolean isHideBuildHistory() {
        return hideBuildHistory;
    }

    public String getDisplayBuildHistory() {
        return hideBuildHistory ? "display: None" : "";
    }

    public String getJobsHeight() {
        if (hideJobs)
            return "0";
        if (hideNodes)
            return "100";
        return getTopHalfHeight();
    }

    public boolean isHideJobs() {
        return hideJobs;
    }

    public String getDisplayJobs() {
        return hideJobs ? "display: None" : "";
    }

    public String getBuildQueueHeight() {
        if (hideBuildQueue)
            return "0";
        if (hideBuildHistory)
            return "100";
        return getBottomHalfHeight();
    }

    public boolean isHideBuildQueue() {
        return hideBuildQueue;
    }

    public String getDisplayBuildQueue() {
        return hideBuildQueue ? "display: None" : "";
    }

    public String getNodesHeight() {
        if (hideNodes)
            return "0";
        if (hideJobs)
            return "100";
        return getBottomHalfHeight();
    }

    public boolean isHideNodes() {
        return hideNodes;
    }

    public String getDisplayNodes() {
        return hideNodes ? "display: None" : "";
    }

    public String getStatusButtonSize() {
        return statusButtonSize;
    }

    public String getLayoutHeightRatio() {
        return layoutHeightRatio;
    }

    public String getFilterBuildHistory() {
        return filterBuildHistory;
    }

    public String getFilterJobStatuses() {
        return filterJobStatuses;
    }

    private String getTopHalfHeight() {
        return layoutHeightRatio.substring(0, 2);
    }

    private String getBottomHalfHeight() {
        return layoutHeightRatio.substring(2, 4);
    }

    @Override
    protected void submit(StaplerRequest req) throws ServletException, IOException {
        JSONObject json = req.getSubmittedForm();
        this.fontSize = json.getInt("fontSize");
        this.buildHistorySize = json.getInt("buildHistorySize");
        this.buildQueueSize = json.getInt("buildQueueSize");
        this.useCondensedTables = json.getBoolean("useCondensedTables");
        this.filterByFailures = json.getBoolean("filterByFailures");
        this.hideBuildHistory = json.getBoolean("hideBuildHistory");
        this.hideJobs = json.getBoolean("hideJobs");
        this.hideBuildQueue = json.getBoolean("hideBuildQueue");
        this.hideNodes = json.getBoolean("hideNodes");
        if (json.get("useRegexFilterBuildHistory") != null ) {
            String buildHistoryRegexToTest = req.getParameter("filterBuildHistory");
            try {
                Pattern.compile(buildHistoryRegexToTest);
                this.filterBuildHistory = buildHistoryRegexToTest;
            } catch (PatternSyntaxException x) {
                Logger.getLogger(ListView.class.getName()).log(Level.WARNING, "Regex filter expression is invalid", x);
            }
        } else {
            this.filterBuildHistory = null;
        }
        if (json.get("useRegexFilterJobStatuses") != null ) {
            String jobStatusesRegexToTest = req.getParameter("filterJobStatuses");
            try {
                Pattern.compile(jobStatusesRegexToTest);
                this.filterJobStatuses = jobStatusesRegexToTest;
            } catch (PatternSyntaxException x) {
                Logger.getLogger(ListView.class.getName()).log(Level.WARNING, "Regex filter expression is invalid", x);
            }
        } else {
            this.filterJobStatuses = null;
        }
        this.statusButtonSize = json.getString("statusButtonSize");
        this.layoutHeightRatio = json.getString("layoutHeightRatio");
        save();
    }

    @Override
    public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean contains(TopLevelItem item) {
        return false;
    }

    @Override
    public boolean hasPermission(final Permission p) { return true; }

    /**
     * This descriptor class is required to configure the View Page
     */
    @Extension
    public static final class DescriptorImpl extends ViewDescriptor {
        @Override
        public String getDisplayName() {
            return Messages.MissionControlView_DisplayName();
        }
    }

    public Api getApi() {
        return new Api(this);
    }

    @Exported(name="builds")
    public Collection<Build> getBuildHistory() {
        ArrayList<Build> l = new ArrayList<Build>();
        Jenkins instance = Jenkins.getInstance();
        if (instance == null)
            return l;

        List<Job> jobs = instance.getAllItems(Job.class);
        RunList builds = new RunList(jobs).limit(getBuildsLimit);
        Pattern r = filterBuildHistory != null ? Pattern.compile(filterBuildHistory) : null;

        for (Object b : builds) {
            Run build = (Run)b;
            Job job = build.getParent();
            String buildUrl = build.getAbsoluteUrl();

            // Skip Maven modules. They are part of parent Maven project
            if (job.getClass().getName().equals("hudson.maven.MavenModule"))
                continue;

            // If filtering is enabled, skip jobs not matching the filter
            if (r != null && !r.matcher(job.getFullName()).find())
                continue;

            Result result = build.getResult();
            l.add(new Build(job.getName(),
                    build.getFullDisplayName(),
                    build.getNumber(),
                    build.getStartTimeInMillis(),
                    build.getDuration(),
                    result == null ? "BUILDING" : result.toString(),
                    buildUrl));
        }

        return l;
    }

    @ExportedBean(defaultVisibility = 999)
    public static class Build {
        @Exported
        public String jobName;
        @Exported
        public String buildName;
        @Exported
        public int number;
        @Exported
        public long startTime;
        @Exported
        public long duration;
        @Exported
        public String result;
        @Exported
        public String buildUrl;

        public Build(String jobName, String buildName, int number, long startTime, long duration, String result, String buildUrl) {
            this.jobName = jobName;
            this.buildName = buildName;
            this.number = number;
            this.startTime = startTime;
            this.duration = duration;
            this.result = result;
            this.buildUrl = buildUrl;
        }
    }

    @Exported(name="allJobsStatuses")
    public Collection<JobStatus> getAllJobsStatuses() {
        String status;
        ArrayList<JobStatus> statuses = new ArrayList<JobStatus>();

        Jenkins instance = Jenkins.getInstance();
        if (instance == null)
            return statuses;

        Pattern r = filterJobStatuses != null ? Pattern.compile(filterJobStatuses) : null;
        List<Job> jobs = instance.getAllItems(Job.class);

        for (Job j : jobs) {
            // Skip matrix configuration sub-jobs and Maven modules
            if (j.getClass().getName().equals("hudson.matrix.MatrixConfiguration")
                    || j.getClass().getName().equals("hudson.maven.MavenModule"))
                continue;

            // Decode pipeline branch names
            String fullName = j.getFullName();
            String jobUrl = j.getAbsoluteUrl();
            try {
                fullName = URLDecoder.decode(j.getFullName(), "UTF-8");
            } catch (java.io.UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            // If filtering is enabled, skip jobs not matching the filter
            if (r != null && !r.matcher(fullName).find())
                continue;

            if (j.isBuilding()) {
                status = "BUILDING";
            } else if (!j.isBuildable()) {
                status = "DISABLED";
            } else {
                Run lb = j.getLastBuild();
                if (lb == null) {
                    status = "NOT_BUILT";
                } else {
                    Result res = lb.getResult();
                    status = res == null ? "UNKNOWN" : res.toString();
                }
            }

            statuses.add(new JobStatus(fullName, status, jobUrl));
        }

        if (filterByFailures) {
            Collections.sort(statuses, new StatusComparator());
        }

        return statuses;
    }

    @ExportedBean(defaultVisibility = 999)
    public static class JobStatus {
        @Exported
        public String jobName;
        @Exported
        public String jobUrl;
        @Exported
        public String status;

        public JobStatus(String jobName, String status, String jobUrl) {
            this.jobName = jobName;
            this.jobUrl = jobUrl;
            this.status = status;
        }
    }

    static class StatusComparator implements Serializable, Comparator<JobStatus> {
        public static Map<String, Integer> statuses = new HashMap<String, Integer>();
        static {
            statuses.put("BUILDING", 1);
            statuses.put("FAILURE", 2);
            statuses.put("UNSTABLE", 3);
            statuses.put("ABORTED", 4);
            statuses.put("SUCCESS", 5);
            statuses.put("NOT_BUILT", 6);
            statuses.put("DISABLED", 7);
        }

        public int compare(JobStatus o1, JobStatus o2) {
            if (o1 == null && o2 == null) {
                return 0;
            }
            if (o1 == null) {
                return -1;
            }
            if (o2 == null) {
                return 1;
            }

            int o1level, o2level;

            o1level = statuses.get(o1.status.toUpperCase());
            o2level = statuses.get(o2.status.toUpperCase());
            if (o1level < o2level) {
                return -1;
            }
            if (o1level > o2level) {
                return +1;
            }
            else {
                return 0;
            }
        }
    }

}