/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014, Red Hat Middleware LLC, 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.ejb3.timerservice.persistence.filestore;

import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.CALENDAR_TIMER;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.DECLARING_CLASS;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.INFO;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.INITIAL_DATE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.NAME;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.NEXT_DATE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.PARAMETER;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.PREVIOUS_RUN;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.PRIMARY_KEY;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.REPEAT_INTERVAL;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_DAY_OF_MONTH;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_DAY_OF_WEEK;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_END_DATE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_HOUR;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_MINUTE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_MONTH;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_SECOND;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_START_DATE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_TIMEZONE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.SCHEDULE_EXPR_YEAR;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.TIMED_OBJECT_ID;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.TIMEOUT_METHOD;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.TIMER;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.TIMER_ID;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.TIMER_STATE;
import static org.jboss.as.ejb3.timerservice.persistence.filestore.EjbTimerXmlPersister.TYPE;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.stream.XMLStreamException;

import org.jboss.as.controller.parsing.ParseUtils;
import org.jboss.as.ejb3.logging.EjbLogger;
import org.jboss.as.ejb3.timerservice.CalendarTimer;
import org.jboss.as.ejb3.timerservice.TimerImpl;
import org.jboss.as.ejb3.timerservice.TimerServiceImpl;
import org.jboss.as.ejb3.timerservice.TimerState;
import org.jboss.as.ejb3.timerservice.persistence.TimeoutMethod;
import org.jboss.marshalling.ByteBufferInput;
import org.jboss.marshalling.MarshallerFactory;
import org.jboss.marshalling.MarshallingConfiguration;
import org.jboss.marshalling.Unmarshaller;
import org.jboss.staxmapper.XMLElementReader;
import org.jboss.staxmapper.XMLExtendedStreamReader;

/**
 * Parser for persistent Jakarta Enterprise Beans timers that are stored in XML.
 *
 * @author Stuart Douglas
 */
public class EjbTimerXmlParser_1_0 implements XMLElementReader<List<TimerImpl>> {

    public static final String NAMESPACE = "urn:jboss:wildfly:ejb-timers:1.0";

    private final TimerServiceImpl timerService;
    private final MarshallerFactory factory;
    private final MarshallingConfiguration configuration;
    private final ClassLoader classLoader;

    public EjbTimerXmlParser_1_0(TimerServiceImpl timerService, MarshallerFactory factory, MarshallingConfiguration configuration, ClassLoader classLoader) {
        this.timerService = timerService;
        this.factory = factory;
        this.configuration = configuration;
        this.classLoader = classLoader;
    }

    @Override
    public void readElement(XMLExtendedStreamReader reader, List<TimerImpl> timers) throws XMLStreamException {
        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case END_ELEMENT: {
                    return;
                }
                case START_ELEMENT: {
                    switch (reader.getName().getLocalPart()) {
                        case TIMER:
                            this.parseTimer(reader, timers);
                            break;
                        case CALENDAR_TIMER:
                            this.parseCalendarTimer(reader, timers);
                            break;
                        default:
                            throw ParseUtils.unexpectedElement(reader);
                    }
                    break;
                }
            }
        }
    }

    private void parseTimer(XMLExtendedStreamReader reader, List<TimerImpl> timers) throws XMLStreamException {
        LoadableElements loadableElements = new LoadableElements();
        TimerImpl.Builder builder = TimerImpl.builder();
        builder.setPersistent(true);
        final Set<String> required = new HashSet<>(Arrays.asList(new String[]{TIMED_OBJECT_ID, TIMER_ID, INITIAL_DATE, REPEAT_INTERVAL, TIMER_STATE}));
        for (int i = 0; i < reader.getAttributeCount(); ++i) {
            String attr = reader.getAttributeValue(i);
            String attrName = reader.getAttributeLocalName(i);
            required.remove(attrName);
            boolean handled = handleCommonAttributes(builder, reader,  i);
            if (!handled) {
                switch (attrName) {
                    case REPEAT_INTERVAL:
                        builder.setRepeatInterval(Long.parseLong(attr));
                        break;
                    default:
                        throw ParseUtils.unexpectedAttribute(reader, i);
                }
            }
        }
        if (!required.isEmpty()) {
            throw ParseUtils.missingRequired(reader, required);
        }

        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case END_ELEMENT: {
                    try {
                        if (loadableElements.info != null) {
                            builder.setInfo((Serializable) deserialize(loadableElements.info));
                        }
                        if (loadableElements.primaryKey != null) {
                            builder.setPrimaryKey(deserialize(loadableElements.primaryKey));
                        }
                        timers.add(builder.build(timerService));
                    } catch (Exception e) {
                        EjbLogger.EJB3_TIMER_LOGGER.timerReinstatementFailed(builder.getTimedObjectId(), builder.getId(), e);
                    }
                    return;
                }
                case START_ELEMENT: {
                    boolean handled = handleCommonElements(reader, loadableElements);
                    if (!handled) {
                        throw ParseUtils.unexpectedElement(reader);
                    }
                    break;
                }
            }
        }
    }

    private Object deserialize(final String info) throws IOException, ClassNotFoundException {

        byte[] data = Base64.getDecoder().decode(info.trim());
        try (final Unmarshaller unmarshaller = factory.createUnmarshaller(configuration)) {
            unmarshaller.start(new ByteBufferInput(ByteBuffer.wrap(data)));
            return unmarshaller.readObject();
        }
    }

    private boolean handleCommonElements(XMLExtendedStreamReader reader, LoadableElements builder) throws XMLStreamException {
        boolean handled = false;
        switch (reader.getName().getLocalPart()) {
            case INFO: {
                builder.info = reader.getElementText();
                handled = true;
                break;
            }
            case PRIMARY_KEY: {
                builder.primaryKey = reader.getElementText();
                handled = true;
                break;
            }
        }
        return handled;
    }


    private void parseCalendarTimer(XMLExtendedStreamReader reader, List<TimerImpl> timers) throws XMLStreamException {
        LoadableElements loadableElements = new LoadableElements();
        CalendarTimer.Builder builder = CalendarTimer.builder();
        builder.setAutoTimer(false).setPersistent(true);
        final Set<String> required = new HashSet<>(Arrays.asList(new String[]{
                TIMED_OBJECT_ID,
                TIMER_ID,
                TIMER_STATE,
                SCHEDULE_EXPR_SECOND,
                SCHEDULE_EXPR_MINUTE,
                SCHEDULE_EXPR_HOUR,
                SCHEDULE_EXPR_DAY_OF_WEEK,
                SCHEDULE_EXPR_DAY_OF_MONTH,
                SCHEDULE_EXPR_MONTH,
                SCHEDULE_EXPR_YEAR}));
        for (int i = 0; i < reader.getAttributeCount(); ++i) {
            String attr = reader.getAttributeValue(i);
            String attrName = reader.getAttributeLocalName(i);
            required.remove(attrName);
            boolean handled = handleCommonAttributes(builder, reader, i);
            if (!handled) {
                switch (attrName) {
                    case SCHEDULE_EXPR_SECOND:
                        builder.setScheduleExprSecond(attr);
                        break;
                    case SCHEDULE_EXPR_MINUTE:
                        builder.setScheduleExprMinute(attr);
                        break;
                    case SCHEDULE_EXPR_HOUR:
                        builder.setScheduleExprHour(attr);
                        break;
                    case SCHEDULE_EXPR_DAY_OF_WEEK:
                        builder.setScheduleExprDayOfWeek(attr);
                        break;
                    case SCHEDULE_EXPR_DAY_OF_MONTH:
                        builder.setScheduleExprDayOfMonth(attr);
                        break;
                    case SCHEDULE_EXPR_MONTH:
                        builder.setScheduleExprMonth(attr);
                        break;
                    case SCHEDULE_EXPR_YEAR:
                        builder.setScheduleExprYear(attr);
                        break;
                    case SCHEDULE_EXPR_START_DATE:
                        builder.setScheduleExprStartDate(new Date(Long.parseLong(attr)));
                        break;
                    case SCHEDULE_EXPR_END_DATE:
                        builder.setScheduleExprEndDate(new Date(Long.parseLong(attr)));
                        break;
                    case SCHEDULE_EXPR_TIMEZONE:
                        builder.setScheduleExprTimezone(attr);
                        break;
                    default:
                        throw ParseUtils.unexpectedAttribute(reader, i);
                }
            }
        }
        if (!required.isEmpty()) {
            throw ParseUtils.missingRequired(reader, required);
        }

        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case END_ELEMENT: {
                    try {
                        if (loadableElements.info != null) {
                            builder.setInfo((Serializable) deserialize(loadableElements.info));
                        }
                        if (loadableElements.primaryKey != null) {
                            builder.setPrimaryKey(deserialize(loadableElements.primaryKey));
                        }
                        if (loadableElements.methodName != null) {
                            Method timeoutMethod = CalendarTimer.getTimeoutMethod(new TimeoutMethod(loadableElements.className, loadableElements.methodName, loadableElements.params.toArray(new String[loadableElements.params.size()])), classLoader);
                            if(timeoutMethod != null) {
                                builder.setTimeoutMethod(timeoutMethod);
                                timers.add(builder.build(timerService));
                            } else {
                                builder.setId("deleted-timer");
                                timers.add(builder.build(timerService));
                                EjbLogger.EJB3_TIMER_LOGGER.timerReinstatementFailed(builder.getTimedObjectId(), builder.getId(), null);
                            }
                        } else {
                            timers.add(builder.build(timerService));
                        }
                    } catch (Exception e) {
                        EjbLogger.EJB3_TIMER_LOGGER.timerReinstatementFailed(builder.getTimedObjectId(), builder.getId(), e);
                    }
                    return;
                }
                case START_ELEMENT: {
                    boolean handled = handleCommonElements(reader, loadableElements);
                    if (!handled) {
                        switch (reader.getName().getLocalPart()) {
                            case TIMEOUT_METHOD: {
                                builder.setAutoTimer(true);
                                parseTimeoutMethod(reader, loadableElements);
                                break;
                            }
                            default:
                                throw ParseUtils.unexpectedElement(reader);
                        }
                    }
                }
            }
        }

    }

    private void parseTimeoutMethod(XMLExtendedStreamReader reader, LoadableElements loadableElements) throws XMLStreamException {

        final Set<String> required = new HashSet<>(Arrays.asList(new String[]{DECLARING_CLASS, NAME}));
        for (int i = 0; i < reader.getAttributeCount(); ++i) {
            String attr = reader.getAttributeValue(i);
            String attrName = reader.getAttributeLocalName(i);
            required.remove(attrName);
            switch (attrName) {
                case DECLARING_CLASS:
                    loadableElements.className = attr;
                    break;
                case NAME:
                    loadableElements.methodName = attr;
                    break;
                default:
                    throw ParseUtils.unexpectedAttribute(reader, i);
            }
        }

        if (!required.isEmpty()) {
            throw ParseUtils.missingRequired(reader, required);
        }

        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case END_ELEMENT: {
                    return;
                }
                case START_ELEMENT: {
                    switch (reader.getName().getLocalPart()) {
                        case PARAMETER: {
                            handleParam(reader, loadableElements);
                            break;
                        }
                        default:
                            throw ParseUtils.unexpectedElement(reader);
                    }

                }
            }
        }

    }

    private void handleParam(XMLExtendedStreamReader reader, LoadableElements loadableElements) throws XMLStreamException {
        final Set<String> required = new HashSet<>(Arrays.asList(new String[]{TYPE}));
        for (int i = 0; i < reader.getAttributeCount(); ++i) {
            String attr = reader.getAttributeValue(i);
            String attrName = reader.getAttributeLocalName(i);
            required.remove(attrName);
            switch (attrName) {
                case TYPE:
                    loadableElements.params.add(attr);
                    break;
                default:
                    throw ParseUtils.unexpectedAttribute(reader, i);
            }
        }

        if (!required.isEmpty()) {
            throw ParseUtils.missingRequired(reader, required);
        }

        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case END_ELEMENT: {
                    return;
                }
                case START_ELEMENT: {
                    throw ParseUtils.unexpectedElement(reader);
                }
            }
        }

    }


    private boolean handleCommonAttributes(TimerImpl.Builder builder, XMLExtendedStreamReader reader, int i) {
        boolean handled = true;
        String attr = reader.getAttributeValue(i);
        switch (reader.getAttributeLocalName(i)) {
            case TIMED_OBJECT_ID:
                builder.setTimedObjectId(attr);
                break;
            case TIMER_ID:
                builder.setId(attr);
                break;
            case INITIAL_DATE:
                builder.setInitialDate(new Date(Long.parseLong(attr)));
                break;
            case NEXT_DATE:
                builder.setNextDate(new Date(Long.parseLong(attr)));
                break;
            case TIMER_STATE:
                builder.setTimerState(TimerState.valueOf(attr));
                break;
            case PREVIOUS_RUN:
                builder.setPreviousRun(new Date(Long.parseLong(attr)));
                break;
            default:
                handled = false;
        }
        return handled;
    }

    private static class LoadableElements {
        String info;
        String primaryKey;
        String className;
        String methodName;
        final List<String> params = new ArrayList<>();


    }
}
