/* Copyright (C) 2014-2022  Stephan Kreutzer
 *
 * This file is part of option_picker_1, a submodule of the
 * digital_publishing_workflow_tools package.
 *
 * option_picker_1 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3 or any later version,
 * as published by the Free Software Foundation.
 *
 * option_picker_1 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 Affero General Public License 3 for more details.
 *
 * You should have received a copy of the GNU Affero General Public License 3
 * along with option_picker_1. If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * @file $/gui/option_picker/option_picker_1/JobFileProcessor.java
 * @brief Processor to read the job file.
 * @author Stephan Kreutzer
 * @since 2019-04-21
 */



import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import java.util.ResourceBundle;
import java.text.MessageFormat;
import java.io.IOException;
import javax.xml.stream.XMLInputFactory;
import java.io.InputStream;
import java.io.FileInputStream;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.Attribute;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import java.io.UnsupportedEncodingException;



class JobFileProcessor
{
    public JobFileProcessor(File jobFile)
    {
        this.jobFile = jobFile;
    }

    /**
     * @details Deliberately allows only a single option, deliberately allows
     *     several different option entries with the same ID.
     */
    public int Run()
    {
        this.title = null;
        this.optionEntries.clear();

        if (this.jobFile == null)
        {
            throw constructTermination("messageJobFileProcessorNoJobFile", null, null, this.getClass().getName());
        }

        try
        {
            this.jobFile = this.jobFile.getCanonicalFile();
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageJobFileCantGetCanonicalPath", ex, null, this.jobFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageJobFileCantGetCanonicalPath", ex, null, this.jobFile.getAbsolutePath());
        }

        if (this.jobFile.exists() != true)
        {
            throw constructTermination("messageJobFileDoesntExist", null, null, this.jobFile.getAbsolutePath());
        }

        if (this.jobFile.isFile() != true)
        {
            throw constructTermination("messageJobPathIsntAFile", null, null, this.jobFile.getAbsolutePath());
        }

        if (this.jobFile.canRead() != true)
        {
            throw constructTermination("messageJobFileIsntReadable", null, null, this.jobFile.getAbsolutePath());
        }


        try
        {
            XMLInputFactory inputFactory = XMLInputFactory.newInstance();
            InputStream in = new FileInputStream(this.jobFile);
            XMLEventReader eventReader = inputFactory.createXMLEventReader(in);

            while (eventReader.hasNext() == true)
            {
                XMLEvent event = eventReader.nextEvent();

                if (event.isStartElement() == true)
                {
                    String tagName = event.asStartElement().getName().getLocalPart();

                    if (tagName.equals("title") == true)
                    {
                        if (this.title != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), tagName);
                        }

                        this.title = HandleText(eventReader, "title");
                    }
                    else if (tagName.equals("option") == true)
                    {
                        Attribute attributeId = event.asStartElement().getAttributeByName(new QName("id"));

                        if (attributeId == null)
                        {
                            throw constructTermination("messageJobFileElementIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), tagName, "id");
                        }

                        OptionEntry option = HandleOption(eventReader);
                        this.optionEntries.add(new OptionEntry(attributeId.getValue(), option.GetCaption(), option.GetDescription()));
                    }
                }
            }
        }
        catch (XMLStreamException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }

        if (this.title == null)
        {
            throw constructTermination("messageJobFileIsMissingAnElement", null, null, jobFile.getAbsolutePath(), "title");
        }

        if (this.optionEntries.size() <= 0)
        {
            throw constructTermination("messageJobFileIsMissingAnElement", null, null, jobFile.getAbsolutePath(), "option");
        }

        return 0;
    }

    private OptionEntry HandleOption(XMLEventReader eventReader) throws XMLStreamException
    {
        String caption = null;
        String description = null;

        while (eventReader.hasNext() == true)
        {
            XMLEvent event = eventReader.nextEvent();

            if (event.isStartElement() == true)
            {
                String tagName = event.asStartElement().getName().getLocalPart();

                if (tagName.equals("caption") == true)
                {
                    if (caption != null)
                    {
                        throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), tagName);
                    }

                    caption = HandleText(eventReader, "caption");
                }
                else if (tagName.equals("description") == true)
                {
                    if (description != null)
                    {
                        throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), tagName);
                    }

                    description = HandleText(eventReader, "description");
                }
            }
            else if (event.isEndElement() == true)
            {
                if (event.asEndElement().getName().getLocalPart().equals("option") == true)
                {
                    break;
                }
            }
        }

        if (caption == null)
        {
            throw constructTermination("messageJobFileIsMissingAnElement", null, null, jobFile.getAbsolutePath(), "caption");
        }

        if (description == null)
        {
            throw constructTermination("messageJobFileIsMissingAnElement", null, null, jobFile.getAbsolutePath(), "description");
        }

        return new OptionEntry(null, caption, description);
    }

    public String HandleText(XMLEventReader eventReader, String tagNameEnd) throws XMLStreamException
    {
        StringBuilder sbText = new StringBuilder();

        while (eventReader.hasNext() == true)
        {
            XMLEvent event = eventReader.nextEvent();

            if (event.isCharacters() == true)
            {
                sbText.append(event.asCharacters().getData());
            }
            else if (event.isEndElement() == true)
            {
                if (event.asEndElement().getName().getLocalPart().equals(tagNameEnd) == true)
                {
                    return sbText.toString();
                }
            }
        }

        throw constructTermination("messageJobFileUnexpectedNoMoreInput", null, null, jobFile.getAbsolutePath());
    }

    public String GetTitle()
    {
        return this.title;
    }

    public List<OptionEntry> GetOptionEntries()
    {
        return this.optionEntries;
    }

    private ProgramTerminationException constructTermination(String id, Exception cause, String message, Object ... arguments)
    {
        if (message == null)
        {
            if (arguments == null)
            {
                message = "option_picker_1: " + getI10nString(id);
            }
            else
            {
                message = "option_picker_1: " + getI10nStringFormatted(id, arguments);
            }
        }

        return new ProgramTerminationException(id, cause, message, L10N_BUNDLE, arguments);
    }

    private Locale getLocale()
    {
        return Locale.getDefault();
    }

    /**
     * @brief This method interprets l10n strings from a .properties file as encoded in UTF-8.
     */
    private String getI10nString(String key)
    {
        if (this.l10nConsole == null)
        {
            this.l10nConsole = ResourceBundle.getBundle(L10N_BUNDLE, this.getLocale());
        }

        try
        {
            return new String(this.l10nConsole.getString(key).getBytes("UTF-8"), "UTF-8");
        }
        catch (UnsupportedEncodingException ex)
        {
            return this.l10nConsole.getString(key);
        }
    }

    private String getI10nStringFormatted(String i10nStringName, Object ... arguments)
    {
        MessageFormat formatter = new MessageFormat("");
        formatter.setLocale(this.getLocale());

        formatter.applyPattern(getI10nString(i10nStringName));
        return formatter.format(arguments);
    }

    protected File jobFile = null;
    protected String title = null;
    protected List<OptionEntry> optionEntries = new ArrayList<OptionEntry>();

    private static final String L10N_BUNDLE = "l10n.l10nJobFileProcessorConsole";
    private ResourceBundle l10nConsole = null;
}
