/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */
package org.elasticsearch.xpack.ml.job.results;

import org.elasticsearch.action.support.ToXContentToBytes;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser.Token;
import org.elasticsearch.xpack.ml.job.config.Job;
import org.elasticsearch.xpack.ml.utils.time.TimeUtils;

import java.io.IOException;
import java.util.Date;
import java.util.Objects;

/**
 * Model Plot POJO.
 */
public class ModelPlot extends ToXContentToBytes implements Writeable {
    /**
     * Result type
     */
    public static final String RESULT_TYPE_VALUE = "model_plot";
    public static final ParseField RESULTS_FIELD = new ParseField(RESULT_TYPE_VALUE);

    public static final ParseField PARTITION_FIELD_NAME = new ParseField("partition_field_name");
    public static final ParseField PARTITION_FIELD_VALUE = new ParseField("partition_field_value");
    public static final ParseField OVER_FIELD_NAME = new ParseField("over_field_name");
    public static final ParseField OVER_FIELD_VALUE = new ParseField("over_field_value");
    public static final ParseField BY_FIELD_NAME = new ParseField("by_field_name");
    public static final ParseField BY_FIELD_VALUE = new ParseField("by_field_value");
    public static final ParseField MODEL_FEATURE = new ParseField("model_feature");
    public static final ParseField MODEL_LOWER = new ParseField("model_lower");
    public static final ParseField MODEL_UPPER = new ParseField("model_upper");
    public static final ParseField MODEL_MEDIAN = new ParseField("model_median");
    public static final ParseField ACTUAL = new ParseField("actual");

    public static final ConstructingObjectParser<ModelPlot, Void> PARSER =
            new ConstructingObjectParser<>(RESULT_TYPE_VALUE, a -> new ModelPlot((String) a[0]));

    static {
        PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID);
        PARSER.declareString((modelPlot, s) -> {}, Result.RESULT_TYPE);
        PARSER.declareField(ModelPlot::setTimestamp, p -> {
            if (p.currentToken() == Token.VALUE_NUMBER) {
                return new Date(p.longValue());
            } else if (p.currentToken() == Token.VALUE_STRING) {
                return new Date(TimeUtils.dateStringToEpoch(p.text()));
            }
            throw new IllegalArgumentException("unexpected token [" + p.currentToken() + "] for ["
                    + Result.TIMESTAMP.getPreferredName() + "]");
        }, Result.TIMESTAMP, ValueType.VALUE);
        PARSER.declareString(ModelPlot::setPartitionFieldName, PARTITION_FIELD_NAME);
        PARSER.declareString(ModelPlot::setPartitionFieldValue, PARTITION_FIELD_VALUE);
        PARSER.declareString(ModelPlot::setOverFieldName, OVER_FIELD_NAME);
        PARSER.declareString(ModelPlot::setOverFieldValue, OVER_FIELD_VALUE);
        PARSER.declareString(ModelPlot::setByFieldName, BY_FIELD_NAME);
        PARSER.declareString(ModelPlot::setByFieldValue, BY_FIELD_VALUE);
        PARSER.declareString(ModelPlot::setModelFeature, MODEL_FEATURE);
        PARSER.declareDouble(ModelPlot::setModelLower, MODEL_LOWER);
        PARSER.declareDouble(ModelPlot::setModelUpper, MODEL_UPPER);
        PARSER.declareDouble(ModelPlot::setModelMedian, MODEL_MEDIAN);
        PARSER.declareDouble(ModelPlot::setActual, ACTUAL);
    }

    private final String jobId;
    private Date timestamp;
    private String id;
    private String partitionFieldName;
    private String partitionFieldValue;
    private String overFieldName;
    private String overFieldValue;
    private String byFieldName;
    private String byFieldValue;
    private String modelFeature;
    private double modelLower;
    private double modelUpper;
    private double modelMedian;
    private double actual;

    public ModelPlot(String jobId) {
        this.jobId = jobId;
    }

    public ModelPlot(StreamInput in) throws IOException {
        jobId = in.readString();
        if (in.readBoolean()) {
            timestamp = new Date(in.readLong());
        }
        id = in.readOptionalString();
        partitionFieldName = in.readOptionalString();
        partitionFieldValue = in.readOptionalString();
        overFieldName = in.readOptionalString();
        overFieldValue = in.readOptionalString();
        byFieldName = in.readOptionalString();
        byFieldValue = in.readOptionalString();
        modelFeature = in.readOptionalString();
        modelLower = in.readDouble();
        modelUpper = in.readDouble();
        modelMedian = in.readDouble();
        actual = in.readDouble();
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        out.writeString(jobId);
        boolean hasTimestamp = timestamp != null;
        out.writeBoolean(hasTimestamp);
        if (hasTimestamp) {
            out.writeLong(timestamp.getTime());
        }
        out.writeOptionalString(id);
        out.writeOptionalString(partitionFieldName);
        out.writeOptionalString(partitionFieldValue);
        out.writeOptionalString(overFieldName);
        out.writeOptionalString(overFieldValue);
        out.writeOptionalString(byFieldName);
        out.writeOptionalString(byFieldValue);
        out.writeOptionalString(modelFeature);
        out.writeDouble(modelLower);
        out.writeDouble(modelUpper);
        out.writeDouble(modelMedian);
        out.writeDouble(actual);
    }

    @Override
    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        builder.startObject();
        builder.field(Job.ID.getPreferredName(), jobId);
        builder.field(Result.RESULT_TYPE.getPreferredName(), RESULT_TYPE_VALUE);
        if (timestamp != null) {
            builder.dateField(Result.TIMESTAMP.getPreferredName(), 
                    Result.TIMESTAMP.getPreferredName() + "_string", timestamp.getTime());
        }
        if (partitionFieldName != null) {
            builder.field(PARTITION_FIELD_NAME.getPreferredName(), partitionFieldName);
        }
        if (partitionFieldValue != null) {
            builder.field(PARTITION_FIELD_VALUE.getPreferredName(), partitionFieldValue);
        }
        if (overFieldName != null) {
            builder.field(OVER_FIELD_NAME.getPreferredName(), overFieldName);
        }
        if (overFieldValue != null) {
            builder.field(OVER_FIELD_VALUE.getPreferredName(), overFieldValue);
        }
        if (byFieldName != null) {
            builder.field(BY_FIELD_NAME.getPreferredName(), byFieldName);
        }
        if (byFieldValue != null) {
            builder.field(BY_FIELD_VALUE.getPreferredName(), byFieldValue);
        }
        if (modelFeature != null) {
            builder.field(MODEL_FEATURE.getPreferredName(), modelFeature);
        }
        builder.field(MODEL_LOWER.getPreferredName(), modelLower);
        builder.field(MODEL_UPPER.getPreferredName(), modelUpper);
        builder.field(MODEL_MEDIAN.getPreferredName(), modelMedian);
        builder.field(ACTUAL.getPreferredName(), actual);
        builder.endObject();
        return builder;
    }

    public String getJobId() {
        return jobId;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Date getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(Date timestamp) {
        this.timestamp = timestamp;
    }

    public String getPartitionFieldName() {
        return partitionFieldName;
    }

    public void setPartitionFieldName(String partitionFieldName) {
        this.partitionFieldName = partitionFieldName;
    }

    public String getPartitionFieldValue() {
        return partitionFieldValue;
    }

    public void setPartitionFieldValue(String partitionFieldValue) {
        this.partitionFieldValue = partitionFieldValue;
    }

    public String getOverFieldName() {
        return overFieldName;
    }

    public void setOverFieldName(String overFieldName) {
        this.overFieldName = overFieldName;
    }

    public String getOverFieldValue() {
        return overFieldValue;
    }

    public void setOverFieldValue(String overFieldValue) {
        this.overFieldValue = overFieldValue;
    }

    public String getByFieldName() {
        return byFieldName;
    }

    public void setByFieldName(String byFieldName) {
        this.byFieldName = byFieldName;
    }

    public String getByFieldValue() {
        return byFieldValue;
    }

    public void setByFieldValue(String byFieldValue) {
        this.byFieldValue = byFieldValue;
    }

    public String getModelFeature() {
        return modelFeature;
    }

    public void setModelFeature(String modelFeature) {
        this.modelFeature = modelFeature;
    }

    public double getModelLower() {
        return modelLower;
    }

    public void setModelLower(double modelLower) {
        this.modelLower = modelLower;
    }

    public double getModelUpper() {
        return modelUpper;
    }

    public void setModelUpper(double modelUpper) {
        this.modelUpper = modelUpper;
    }

    public double getModelMedian() {
        return modelMedian;
    }

    public void setModelMedian(double modelMedian) {
        this.modelMedian = modelMedian;
    }

    public double getActual() {
        return actual;
    }

    public void setActual(double actual) {
        this.actual = actual;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other instanceof ModelPlot == false) {
            return false;
        }
        // id excluded here as it is generated by the datastore
        ModelPlot that = (ModelPlot) other;
        return Objects.equals(this.jobId, that.jobId) &&
                Objects.equals(this.timestamp, that.timestamp) &&
                Objects.equals(this.partitionFieldValue, that.partitionFieldValue) &&
                Objects.equals(this.partitionFieldName, that.partitionFieldName) &&
                Objects.equals(this.overFieldValue, that.overFieldValue) &&
                Objects.equals(this.overFieldName, that.overFieldName) &&
                Objects.equals(this.byFieldValue, that.byFieldValue) &&
                Objects.equals(this.byFieldName, that.byFieldName) &&
                Objects.equals(this.modelFeature, that.modelFeature) &&
                this.modelLower == that.modelLower &&
                this.modelUpper == that.modelUpper &&
                this.modelMedian == that.modelMedian &&
                this.actual == that.actual;
    }

    @Override
    public int hashCode() {
        // id excluded here as it is generated by the datastore
        return Objects.hash(jobId, timestamp, partitionFieldName, partitionFieldValue,
                overFieldName, overFieldValue, byFieldName, byFieldValue,
                modelFeature, modelLower, modelUpper, modelMedian, actual);
    }
}
