/*
 *  Copyright 2019 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.ametys.cms.data.type;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.cocoon.xml.AttributesImpl;
import org.apache.cocoon.xml.XMLUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;
import org.w3c.dom.Element;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

import org.ametys.cms.contenttype.DocbookRichTextUpdater;
import org.ametys.cms.contenttype.RichTextUpdater;
import org.ametys.cms.data.LocalMediaObjectHandler;
import org.ametys.cms.data.RichText;
import org.ametys.cms.data.RichTextHelper;
import org.ametys.cms.data.RichTextImportHandlerFactory;
import org.ametys.cms.data.RichTextImportHandlerFactory.RichTextImportHandler;
import org.ametys.cms.model.CMSDataContext;
import org.ametys.cms.transformation.RichTextTransformer;
import org.ametys.cms.transformation.docbook.DocbookTransformer;
import org.ametys.core.model.type.AbstractElementType;
import org.ametys.core.model.type.ModelItemTypeHelper;
import org.ametys.core.util.DateUtils;
import org.ametys.core.util.dom.DOMUtils;
import org.ametys.plugins.repository.AmetysRepositoryException;
import org.ametys.plugins.repository.data.ametysobject.DataAwareAmetysObject;
import org.ametys.plugins.repository.data.ametysobject.ModelAwareDataAwareAmetysObject;
import org.ametys.plugins.repository.data.ametysobject.ModelLessDataAwareAmetysObject;
import org.ametys.plugins.repository.model.RepositoryDataContext;
import org.ametys.runtime.model.compare.DataChangeType;
import org.ametys.runtime.model.compare.DataChangeTypeDetail;
import org.ametys.runtime.model.exception.BadItemTypeException;
import org.ametys.runtime.model.type.DataContext;

import com.google.common.base.CharMatcher;

/**
 * Base class for rich text type of elements
 */
public class BaseRichTextElementType extends AbstractElementType<RichText> implements RichTextElementType
{
    /** encoding's key used for JSON conversion and SAX events generation */
    protected static final String ENCODING_KEY = "encoding";
    /** rich text content's key used for JSON conversion and SAX events generation */
    protected static final String RICH_TEXT_CONTENT_KEY = "value";
    
    /** Rich text transformer */
    protected RichTextTransformer _richTextTransformer;
    /** Rich text updater */
    protected RichTextUpdater _richTextUpdater;
    /** Rich text metadata handler */
    protected RichTextImportHandlerFactory _richTextHandlerFactory;
    /** Rich text helper */
    protected RichTextHelper _richTextHelper;

    @Override
    public void service(ServiceManager manager) throws ServiceException
    {
        super.service(manager);
        _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
        _richTextUpdater = (RichTextUpdater) manager.lookup(DocbookRichTextUpdater.ROLE);
        _richTextHandlerFactory = (RichTextImportHandlerFactory) manager.lookup(RichTextImportHandlerFactory.ROLE);
        _richTextHelper = (RichTextHelper) manager.lookup(RichTextHelper.ROLE);
    }
    
    @Override
    public RichText convertValue(Object value)
    {
        if (value instanceof byte[])
        {
            return _fromByteArray((byte[]) value, DataContext.newInstance());
        }
        else if (value instanceof String)
        {
            return _fromString((String) value, DataContext.newInstance());
        }
        
        return super.convertValue(value);
    }
    
    @Override
    public String toString(RichText value)
    {
        return _richTextHelper.richTextToString(value);
    }
    
    @Override
    protected RichText _singleValueFromJSON(Object json, DataContext context) throws BadItemTypeException
    {
        if (json instanceof String content)
        {
            if (StringUtils.isEmpty(content))
            {
                return null;
            }
            
            return _fromHTML(content, context);
        }
        else
        {
            throw new BadItemTypeException("Unable to convert the given json value '" + json + "' into a " + getManagedClass().getName());
        }
    }
    
    private RichText _fromByteArray(byte[] content, DataContext context)
    {
        String contentAsString = StringUtils.EMPTY;
        String strValue = IOUtils.toString(content, "UTF-8");
        contentAsString = CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue);

        return _fromString(contentAsString, context);
    }
    
    private RichText _fromString(String content, DataContext context)
    {
        String contentAsHTML = "<div>" + content.replaceAll("\r?\n", "<br />") + "</div>";
        return _fromHTML(contentAsHTML, context);
    }
    
    private RichText _fromHTML(String content, DataContext context)
    {
        RichText richText = new RichText();
        
        String dataPath = context.getDataPath();
        if (StringUtils.isNotEmpty(dataPath))
        {
            RepositoryDataContext repoContext = context instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(context);
            Optional<DataAwareAmetysObject> optObject = repoContext.getObject();
            if (optObject.isPresent() && optObject.get().hasValue(dataPath))
            {
                DataAwareAmetysObject object = optObject.get();
                if (object instanceof ModelAwareDataAwareAmetysObject modelAwareAO)
                {
                    richText = modelAwareAO.getValue(dataPath);
                }
                else if (object instanceof ModelLessDataAwareAmetysObject modelLessAO)
                {
                    richText = modelLessAO.getValueOfType(dataPath, getId());
                }
            }
        }
        
        try
        {
            _richTextTransformer.transform(content, richText);
        }
        catch (AmetysRepositoryException | IOException e)
        {
            throw new IllegalArgumentException("Unable to transform the rich text value from the given HTML: " + content, e);
        }
        
        return richText;
    }
    
    @Override
    protected Object _singleTypedValueToJSON(RichText value, DataContext context)
    {
        Map<String, Object> richTextInfos = new LinkedHashMap<>();
        
        Optional.ofNullable(value.getMimeType()).ifPresent(mimeType -> richTextInfos.put(ResourceElementTypeHelper.MIME_TYPE_KEY, mimeType));
        Optional.ofNullable(value.getLastModificationDate()).map(DateUtils::zonedDateTimeToString).ifPresent(lastModificationDate -> richTextInfos.put(ResourceElementTypeHelper.LAST_MODIFICATION_DATE_KEY, lastModificationDate));
        Optional.ofNullable(value.getLength()).ifPresent(length -> richTextInfos.put(ResourceElementTypeHelper.SIZE_KEY, length));
        
        int excerptLength = context instanceof CMSDataContext cmsContext ? cmsContext.getRichTextMaxLength() : 0;
        String valueAsString = _richTextHelper.richTextToString(value, excerptLength);
        Optional.ofNullable(valueAsString).ifPresent(content -> richTextInfos.put(RICH_TEXT_CONTENT_KEY, content));
        
        return richTextInfos;
    }
    
    @Override
    protected Object _singleTypedValueToJSONForEdition(RichText value, DataContext context)
    {
        try
        {
            StringBuilder result = new StringBuilder(2048);
            _richTextTransformer.transformForEditing(value, context, result);
            return result.toString();
        }
        catch (IOException e)
        {
            throw new AmetysRepositoryException("Unable to transform the rich text valueinto a string", e);
        }
    }
    
    @Override
    protected RichText _singleValueFromXML(Element element, Optional<Object> additionalData)
    {
        if (element != null)
        {
            RichText richText = new RichText();
            
            String mimeType = StringUtils.defaultIfBlank(element.getAttribute(ResourceElementTypeHelper.MIME_TYPE_KEY), "text/xml");
            String encoding = StringUtils.defaultIfBlank(element.getAttribute(ENCODING_KEY), StandardCharsets.UTF_8.name());
            
            richText.setMimeType(mimeType);
            richText.setEncoding(encoding);
            richText.setLastModificationDate(ZonedDateTime.now());
            
            if (mimeType.equals("text/xml") || mimeType.equals("application/xml"))
            {
                try (OutputStream os = richText.getOutputStream())
                {
                    Element childElement = DOMUtils.getFirstChildElement(element);
                    if (childElement != null)
                    {
                        Properties format = new Properties();
                        format.put(OutputKeys.METHOD, "xml");
                        format.put(OutputKeys.ENCODING, encoding);
                        format.put(OutputKeys.INDENT, "no");
                        
                        Transformer transformer = TransformerFactory.newInstance().newTransformer();
                        transformer.setOutputProperties(format);
                        
                        DOMSource domSource = new DOMSource(childElement);
                        
                        TransformerHandler transformerhandler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
                        transformerhandler.getTransformer().setOutputProperties(format);
                        
                        StreamResult result = new StreamResult(os);
                        transformerhandler.setResult(result);
                        
                        @SuppressWarnings("unchecked")
                        Map<String, InputStream> files = additionalData.isPresent() ? (Map<String, InputStream>) additionalData.get() : Map.of();
                        RichTextImportHandler handler = _richTextHandlerFactory.createHandlerProxy(transformerhandler, richText, files);
                        SAXResult saxResult = new SAXResult(handler);
                        
                        transformer.transform(domSource, saxResult);
                    }
                    else
                    {
                        return null;
                    }
                }
                catch (TransformerException | IOException e)
                {
                    throw new IllegalArgumentException("Unable to get the rich text value from the given XML", e);
                }
            }
            else
            {
                String data = element.getTextContent();
                if (StringUtils.isNotBlank(data))
                {
                    try (OutputStream os = richText.getOutputStream())
                    {
                        os.write(data.getBytes(encoding));
                    }
                    catch (IOException e)
                    {
                        throw new IllegalArgumentException("Unable to get the rich text value from the given XML", e);
                    }
                }
                else
                {
                    return null;
                }
            }
            
            return richText;
        }
        else
        {
            return null;
        }
    }
    
    @Override
    protected void _valueToSAXForEdition(ContentHandler contentHandler, String tagName, Object value, DataContext context, AttributesImpl attributes) throws SAXException
    {
        if (value instanceof RichText)
        {
            _singleValueToSAXForEdition(contentHandler, tagName, (RichText) value, context, attributes);
        }
        else if (value instanceof RichText[])
        {
            for (RichText richText : (RichText[]) value)
            {
                _singleValueToSAXForEdition(contentHandler, tagName, richText, context, attributes);
            }
        }
    }
    
    private void _singleValueToSAXForEdition(ContentHandler contentHandler, String tagName, RichText richText, DataContext context, AttributesImpl attributes) throws SAXException
    {
        AttributesImpl localAttributes = new AttributesImpl(attributes);
        _addAttributesToSAX(richText, localAttributes);
        XMLUtils.startElement(contentHandler, tagName, localAttributes);
        
        StringBuilder result = new StringBuilder(2048);
        
        try
        {
            _richTextTransformer.transformForEditing(richText, context, result);
        }
        catch (IOException e)
        {
            throw new SAXException("Unable to transform a rich text into a string", e);
        }
        
        XMLUtils.data(contentHandler, result.toString());
        
        XMLUtils.endElement(contentHandler, tagName);
    }

    @Override
    protected void _singleTypedNotEnumeratedValueToSAX(ContentHandler contentHandler, String tagName, RichText richText, DataContext context, AttributesImpl attributes) throws SAXException
    {
        AttributesImpl localAttributes = new AttributesImpl(attributes);
        _addAttributesToSAX(richText, localAttributes);
        ContentHandler proxiedHandler = _getLocalMediaObjectContentHandler(contentHandler, context);
        try
        {
            XMLUtils.startElement(contentHandler, tagName, localAttributes);

            String mimeType = richText.getMimeType();
            if (mimeType.equals("text/xml") || mimeType.equals("application/xml"))
            {
                _richTextTransformer.transformForRendering(richText, proxiedHandler);
            }
            else if (mimeType.equals("text/plain"))
            {
                try (InputStream is = richText.getInputStream())
                {
                    XMLUtils.data(proxiedHandler, IOUtils.toString(is, richText.getEncoding()));
                }
            }
            else
            {
                throw new SAXException("Mime-type " + mimeType + " is not supported for the sax rich text");
            }

            XMLUtils.endElement(contentHandler, tagName);
        }
        catch (IOException e)
        {
            throw new SAXException("Unable to generate SAX events for the given rich text due to an I/O error", e);
        }
    }
    
    /**
     * Retrieves the content handler to use to transform local media objects
     * @param contentHandler the initial {@link ContentHandler}
     * @param context The context of the data to SAX
     * @return the content handler
     */
    protected ContentHandler _getLocalMediaObjectContentHandler(ContentHandler contentHandler, DataContext context)
    {
        return new LocalMediaObjectHandler(contentHandler, context);
    }
    
    private void _addAttributesToSAX(RichText richText, AttributesImpl attributes)
    {
        String mimeType = richText.getMimeType();
        attributes.addCDATAAttribute(ResourceElementTypeHelper.MIME_TYPE_KEY, mimeType);
        Optional.ofNullable(richText.getLastModificationDate())
                .map(DateUtils::zonedDateTimeToString)
                .ifPresent(lastModificationDate -> attributes.addCDATAAttribute(ResourceElementTypeHelper.LAST_MODIFICATION_DATE_KEY, lastModificationDate));

        String encoding = _getNonNullEncoding(richText);
        attributes.addCDATAAttribute(ENCODING_KEY, encoding);
    }
    
    private String _getNonNullEncoding(RichText richText)
    {
        String encoding = richText.getEncoding();
        if (encoding == null)
        {
            encoding = StandardCharsets.UTF_8.name();
        }
        return encoding;
    }
    
    @Override
    protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareMultipleValues(RichText[] value1, RichText[] value2)
    {
        throw new UnsupportedOperationException("Unable to compare multiple values of type '" + getId() + "'");
    }
    
    @Override
    protected Stream<Triple<DataChangeType, DataChangeTypeDetail, String>> _compareSingleValues(RichText value1, RichText value2)
    {
        if (ModelItemTypeHelper.areSingleObjectsBothNotNullAndDifferents(value1, value2))
        {
            return ResourceElementTypeHelper.compareSingleRichTexts(value1, value2);
        }
        else
        {
            return ModelItemTypeHelper.compareSingleObjects(value1, value2, StringUtils.EMPTY).stream();
        }
    }

    public boolean isSimple()
    {
        return false;
    }
    
    public RichTextUpdater getRichTextUpdater()
    {
        return _richTextUpdater;
    }
}
