/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.test.integration.batch.common;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import jakarta.batch.runtime.BatchStatus;
import jakarta.batch.runtime.Metric;
import jakarta.batch.runtime.Metric.MetricType;
import jakarta.batch.runtime.StepExecution;
import jakarta.json.Json;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;
import jakarta.json.stream.JsonParser.Event;

/**
 * @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
 */
public class StepExecutionMarshaller {

    static final String ID = "id";
    static final String NAME = "name";
    static final String STATUS = "status";
    static final String START_TIME = "startTime";
    static final String END_TIME = "endTime";
    static final String EXIT_STATUS = "exitStatus";
    static final String PERSISTENT_USER_DATA = "persistentUserData";
    static final String METRICS = "METRICS";
    private static final String METRIC = "metric";
    private static final String METRIC_TYPE = "type";
    private static final String METRIC_VALUE = "value";

    public static String marshall(final StepExecution stepExecution) throws IOException {
        final StringWriter writer = new StringWriter();
        final JsonGenerator generator = Json.createGenerator(writer);
        generator.writeStartObject()
                .write(ID, stepExecution.getStepExecutionId())
                .write(NAME, stepExecution.getStepName())
                .write(STATUS, stepExecution.getBatchStatus().toString())
                .write(START_TIME, stepExecution.getStartTime().getTime())
                .write(END_TIME, stepExecution.getEndTime().getTime())
                .write(EXIT_STATUS, stepExecution.getExitStatus())
                .write(PERSISTENT_USER_DATA, serialize(stepExecution.getPersistentUserData()));
        generator.writeStartObject(METRICS);
        for (Metric metric : stepExecution.getMetrics()) {
            generator.writeStartObject(METRIC);
            generator.write(METRIC_TYPE, metric.getType().toString());
            generator.write(METRIC_VALUE, metric.getValue());
            generator.writeEnd();
        }
        generator.writeEnd();

        // End main object
        generator.writeEnd();
        generator.close();
        return writer.toString();
    }

    public static StepExecution unmarshall(final String json) throws IOException, ClassNotFoundException {
        final JsonParser parser = Json.createParser(new StringReader(json));
        final StepExecutionBuilder builder = StepExecutionBuilder.create();

        String key = null;
        while (parser.hasNext()) {
            final Event event = parser.next();
            switch (event) {
                case KEY_NAME:
                    key = parser.getString();
                    break;
                case VALUE_FALSE:
                case VALUE_NULL:
                case VALUE_NUMBER:
                case VALUE_STRING:
                case VALUE_TRUE:
                    final String value = parser.getString();
                    if (key == null) {
                        throw new IllegalStateException(String.format("No key for value '%s'. Parsing position: %s%n\t%s", value, parser.getLocation(), json));
                    }
                    switch (key) {
                        case ID:
                            if (value != null) {
                                builder.setId(Long.parseLong(value));
                            }
                            break;
                        case NAME:
                            builder.setName(value);
                            break;
                        case STATUS:
                            if (value != null) {
                                builder.setStatus(BatchStatus.valueOf(value));
                            }
                            break;
                        case EXIT_STATUS:
                            builder.setExitStatus(value);
                            break;
                        case END_TIME:
                            if (value != null) {
                                builder.setEndTime(Long.parseLong(value));
                            }
                            break;
                        case START_TIME:
                            if (value != null) {
                                builder.setStartTime(Long.parseLong(value));
                            }
                            break;
                        case PERSISTENT_USER_DATA:
                            builder.setPersistentUserData(deserialize(value));
                        case METRICS:
                            String k = null;
                            String metricType = null;
                            String metricValue = null;
                            while (parser.hasNext()) {
                                final Event e = parser.next();
                                switch (e) {
                                    case KEY_NAME:
                                        k = parser.getString();
                                        break;
                                    case VALUE_FALSE:
                                    case VALUE_NULL:
                                    case VALUE_NUMBER:
                                    case VALUE_STRING:
                                    case VALUE_TRUE:
                                        if (k == null) {
                                            throw new IllegalStateException(String.format("No key for value '%s'. Parsing position: %s%n\t%s", value, parser.getLocation(), json));
                                        }
                                        switch (k) {
                                            case METRIC_TYPE:
                                                metricType = parser.getString();
                                                break;
                                            case METRIC_VALUE:
                                                metricValue = parser.getString();
                                                break;
                                        }
                                        if (metricType != null && metricValue != null) {
                                            final MetricType type = MetricType.valueOf(metricType);
                                            final long v = Long.parseLong(parser.getString());
                                            final Metric m = new Metric() {
                                                @Override
                                                public MetricType getType() {
                                                    return type;
                                                }

                                                @Override
                                                public long getValue() {
                                                    return v;
                                                }
                                            };
                                            builder.addMetric(m);
                                        }
                                        break;
                                }
                            }
                            break;
                    }
                    break;
            }
        }
        parser.close();
        return builder.build();
    }

    private static String serialize(final Serializable serializable) throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final ObjectOutputStream out = new ObjectOutputStream(baos);
        out.writeObject(serializable);
        out.flush();
        return baos.toString();
    }

    private static Serializable deserialize(final String data) throws IOException, ClassNotFoundException {
        if (data == null) {
            return null;
        }
        final ByteArrayInputStream bais = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
        final ObjectInputStream in = new ObjectInputStream(bais);
        return (Serializable) in.readObject();
    }

    static class StepExecutionBuilder {
        private long id;
        private String name;
        private BatchStatus status;
        private long startTime;
        private long endTime;
        private String exitStatus;
        private Serializable persistentUserData;
        private final Collection<Metric> metrics;

        private StepExecutionBuilder() {
            id = -1L;
            name = null;
            status = null;
            startTime = 0L;
            endTime = 0L;
            exitStatus = null;
            persistentUserData = null;
            metrics = new ArrayList<Metric>();
        }

        public static StepExecutionBuilder create() {
            return new StepExecutionBuilder();
        }

        public StepExecutionBuilder setId(final long id) {
            this.id = id;
            return this;
        }

        public StepExecutionBuilder setName(final String name) {
            this.name = name;
            return this;
        }

        public StepExecutionBuilder setStatus(final BatchStatus status) {
            this.status = status;
            return this;
        }

        public StepExecutionBuilder setStartTime(final long startTime) {
            this.startTime = startTime;
            return this;
        }

        public StepExecutionBuilder setEndTime(final long endTime) {
            this.endTime = endTime;
            return this;
        }

        public StepExecutionBuilder setExitStatus(final String exitStatus) {
            this.exitStatus = exitStatus;
            return this;
        }

        public StepExecutionBuilder setPersistentUserData(final Serializable persistentUserData) {
            this.persistentUserData = persistentUserData;
            return this;
        }

        public StepExecutionBuilder addMetric(final Metric metric) {
            metrics.add(metric);
            return this;
        }

        public StepExecution build() {
            final long id = this.id;
            final String name = this.name;
            final BatchStatus status = this.status;
            final long startTime = this.startTime;
            final long endTime = this.endTime;
            final String exitStatus = this.exitStatus;
            final Serializable persistentUserData = this.persistentUserData;
            final Metric[] metrics = this.metrics.toArray(new Metric[this.metrics.size()]);
            return new StepExecution() {
                @Override
                public long getStepExecutionId() {
                    return id;
                }

                @Override
                public String getStepName() {
                    return name;
                }

                @Override
                public BatchStatus getBatchStatus() {
                    return status;
                }

                @Override
                public Date getStartTime() {
                    return new Date(startTime);
                }

                @Override
                public Date getEndTime() {
                    return new Date(endTime);
                }

                @Override
                public String getExitStatus() {
                    return exitStatus;
                }

                @Override
                public Serializable getPersistentUserData() {
                    return persistentUserData;
                }

                @Override
                public Metric[] getMetrics() {
                    return Arrays.copyOf(metrics, metrics.length);
                }
            };
        }
    }
}
