/**
 * Copyright (c) 2011-2021, JFXtras
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *    Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *    Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *    Neither the name of the organization nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JFXTRAS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package jfxtras.icalendarfx.properties;

import java.lang.reflect.Method;
import java.time.DateTimeException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import jfxtras.icalendarfx.VElement;
import jfxtras.icalendarfx.VParent;
import jfxtras.icalendarfx.VParentBase;
import jfxtras.icalendarfx.content.SingleLineContent;
import jfxtras.icalendarfx.parameters.NonStandardParameter;
import jfxtras.icalendarfx.parameters.VParameter;
import jfxtras.icalendarfx.parameters.ValueParameter;
import jfxtras.icalendarfx.properties.calendar.CalendarScale;
import jfxtras.icalendarfx.properties.calendar.ProductIdentifier;
import jfxtras.icalendarfx.properties.calendar.Version;
import jfxtras.icalendarfx.properties.component.misc.NonStandardProperty;
import jfxtras.icalendarfx.properties.component.relationship.UniqueIdentifier;
import jfxtras.icalendarfx.utilities.ICalendarUtilities;
import jfxtras.icalendarfx.utilities.StringConverter;

/**
 * Base iCalendar property class
 * Contains property value, value parameter (ValueType) and other-parameters
 * Also contains several support methods used by other properties
 * 
 * concrete subclasses
 * @see UniqueIdentifier
 * @see CalendarScale
 * @see Method
 * @see ProductIdentifier
 * @see Version
 * 
 * @author David Bal
 *
 * @param <U> - type of implementing subclass
 * @param <T> - type of property value
 */
public abstract class VPropertyBase<T,U> extends VParentBase<U> implements VProperty<T>
{
    private VParent myParent;
    @Override
    public void setParent(VParent parent)
    {
    	myParent = parent;
	}
    @Override
    public VParent getParent()
    {
    	return myParent;
	}
    
    /**
     * PROPERTY VALUE
     * 
     * Example: for the property content LOCATION:The park the property
     * value is the string "The park"
     */
    @Override
    public T getValue()
    {
    	return value;
	}
    private T value; // initialized in constructor
    @Override
    public void setValue(T value)
    {
        this.value = value;
    }
    public U withValue(T value)
    {
    	setValue(value);
    	return (U) this;
	} // in constructor

    /** The propery's value converted by string converted to content string */
    protected String valueContent()
    {
        /* default code below works for all properties with a single value.  Properties with multiple embedded values,
         * such as RequestStatus, require an overridden method */
    	String value = null;
    	if (getValue() == null)
		{
    		value = getUnknownValue();
		} else if (getConverter().toString(getValue()) == null)
		{
			value = getUnknownValue();
		} else
		{
	        value = getConverter().toString(getValue());			
		}
        return (value == null) ? "" : value;
    }

    // class of value.  If collection, returns type of element instead. 
    // Used to verify value class is allowed for the property type
    private Class<T> valueClass;
    private Class<?> getValueClass()
    {
        if (valueClass == null)
        {
            if (getValue() != null)
            {
                if (getValue() instanceof Collection)
                {
                    if (! ((Collection<?>) getValue()).isEmpty())
                    {
                        return ((Collection<?>) getValue()).iterator().next().getClass();
                    } else
                    {
                        return null;
                    }
                }
                return getValue().getClass();
            }
            return null;
        } else
        {
            return valueClass;
        }
    }
    
    /** The name of the property, such as DESCRIPTION
     * Remains the default value unless set by a non-standard property*/
    @Override
    public String name()
    {
    	if (propertyName == null)
    	{
    		return propertyType.toString();
    	}
        return propertyName;
    }
    protected String propertyName;
    
//    /**
//     * PROPERTY TYPE
//     * 
//     *  The enumerated type of the property.
//     *  Some essential methods are in the enumerated type.
//     */
//    public PropertyType propertyType()
//    {
//    	return propertyType;
//	}
    final private VPropertyElement propertyType;
    
    /*
     * Unknown values
     * contains exact string for unknown property value
     */
    private String unknownValue;
    protected String getUnknownValue()
    {
    	return unknownValue;
    }
    private void setUnknownValue(String value)
    {
    	unknownValue = value;
	}
    
    /**
     * VALUE TYPE
     * Value Data Types
     * RFC 5545, 3.2.20, page 29
     * 
     * To specify the value for text values in a property or property parameter.
     * This parameter is optional for properties when the default value type is used.
     * 
     * Examples:
     * VALUE=DATE-TIME  (Date-Time is default value, so it isn't necessary to specify)
     * VALUE=DATE
     */
    final protected ValueType defaultValueType;
    final protected Collection<ValueType> allowedValueTypes;
    
    @Override
    public ValueParameter getValueType() { return valueType; }
    private ValueParameter valueType;
    @Override
    public void setValueType(ValueParameter valueType)
    {
        if (valueType == null || isValueTypeValid(valueType.getValue()))
        {
        	orderChild(this.valueType, valueType);
            this.valueType = valueType;
            valueParamenterConverter(valueType); // convert new value
        } else
        {
            throw new IllegalArgumentException("Invalid Value Date Type:" + valueType.getValue() + ", allowed = " + allowedValueTypes);
        }
    }
    public void setValueType(ValueType value)
    {
        setValueType(new ValueParameter(value));
    }
    public U withValueType(ValueType value)
    {
        setValueType(value);
        return (U) this;
    } 

    // Synch value with type produced by string converter
    private void valueParamenterConverter(ValueParameter newValueParameter)
    {
        if (! isCustomConverter())
        {
            // Convert property value string, if present
            if (modifiedValue() != null)
            {
                String modifiedValue = modifiedValue();
				T newPropValue = getConverter().fromString(modifiedValue);
                this.value = newPropValue;
            }
        }
        
        // verify value class is allowed
        if (newValueParameter != null && getValueClass() != null) // && ! newValue.getValue().allowedClasses().contains(getValueClass()))
        {
            boolean isMatch = newValueParameter.getValue().allowedClasses()
                    .stream()
                    .map(c -> getValueClass().isAssignableFrom(c))
                    .findAny()
                    .isPresent();
            if (! isMatch)
            {
                throw new IllegalArgumentException("Value class " + getValueClass().getSimpleName() +
                        " doesn't match allowed value classes: " + newValueParameter.getValue().allowedClasses());
            }
        }    	
    }
    
    /**
     * <h2>NON-STANDARD PARAMETERS</h2>
     * 
     * <p>x-param     = x-name "=" param-value *("," param-value)<br>
     ; A non-standard, experimental parameter.</p>
     */
    private List<NonStandardParameter> nonStandardParams;
    @Override
    public List<NonStandardParameter> getNonStandard()
    {
        return nonStandardParams;
    }
    @Override
    public void setNonStandard(List<NonStandardParameter> nonStandardParams)
    {
    	if (this.nonStandardParams != null)
    	{
    		this.nonStandardParams.forEach(e -> orderChild(e, null)); // remove old elements
    	}
        this.nonStandardParams = nonStandardParams;
    	if (nonStandardParams != null)
    	{
        	nonStandardParams.forEach(e -> orderChild(e));
    	}
    }
    /**
     * Sets the value of the {@link #NonStandardParameter()}
     * 
     * @return - this class for chaining
     */
    public U withNonStandard(List<NonStandardParameter> nonStandardParams)
    {
    	if (getNonStandard() == null)
    	{
        	setNonStandard(new ArrayList<>());
    	}
    	getNonStandard().addAll(nonStandardParams);
    	if (nonStandardParams != null)
    	{
    		nonStandardParams.forEach(c -> orderChild(c));
    	}
        return (U) this;
    }
    /**
     * NON-STANDARD PARAMETERS
     * 
     * Sets the value of the {@link #NonStandardParameter()} by parsing a vararg of
     * iCalendar content text representing individual {@link NonStandardParameter} objects.
     * 
     * @return - this class for chaining
     */
    public U withNonStandard(String...nonStandardParams)
    {
        List<NonStandardParameter> list = Arrays.stream(nonStandardParams)
                .map(c -> NonStandardParameter.parse(c))
                .collect(Collectors.toList());
        return withNonStandard(list);
    }
    /**
     * Sets the value of the {@link #NonStandardParameter()} from a vararg of {@link NonStandardParameter} objects.
     * 
     * @return - this class for chaining
     */    
    public U withNonStandard(NonStandardParameter...nonStandardParams)
    {
    	return withNonStandard(Arrays.asList(nonStandardParams));
    }
    
    // property value as string - kept if string converter changes the value can change
    // needed to make subsequent conversions if value type changes.
    protected String actualValueContent = null;
    // Note: in subclasses additional text can be concatenated to string (e.g. ZonedDateTime classes add time zone as prefix)
    protected String modifiedValue()
    {
    	if (actualValueContent == null)
    	{
    		T value2 = getValue();
			if (value2 != null)
    		{
    			return getConverter().toString(getValue());
    		}
    		return null;
    	} else
    	{
			return actualValueContent;
    	}
	}
    
    
    /**
     * STRING CONVERTER
     * 
     * Get the property's value string converter.  There is a default converter in ValueType associated
     * with the default value type of the property.  For most value types that converter is
     * acceptable.  However, for the TEXT value type it often needs to be replaced.
     * For example, the value type for TimeZoneIdentifier is TEXT, but the Java object is
     * ZoneId.  A different converter is required to make the conversion to ZoneId.
     */ 
    protected StringConverter<T> getConverter()
    {
        if (converter == null)
        {
            ValueType valueType = (getValueType() == null) ? defaultValueType : getValueType().getValue();
            return valueType.getConverter(); // use default converter assigned to value type if no customer converter assigned
        }
        return converter;
    }
    private StringConverter<T> converter;
    protected void setConverter(StringConverter<T> converter) { this.converter = converter; }
    private boolean isCustomConverter()
    {
        return converter != null;
    }
    
    /*
     * CONSTRUCTORS
     */
    
//    protected VPropertyBase(Collection<ValueType> allowedValueTypes, ValueType defaultValueType)
    protected VPropertyBase()
    {
    	super();
    	propertyType = VPropertyElement.fromClass(getClass());
    	this.allowedValueTypes = propertyType.allowedValueTypes();
    	this.defaultValueType = propertyType.defaultValueType();
//    	this.allowedValueTypes = VPropertyElement.propertyAllowedValueTypes(getClass());
//    	this.defaultValueType = VPropertyElement.defaultValueType(getClass());
//    	this.defaultValueType = defaultValueType;
//        propertyType = PropertyType.enumFromClass(getClass());
//        if (propertyType != PropertyType.NON_STANDARD)
//        {
//            setPropertyName(propertyType.toString());
//        }
        contentLineGenerator  = new SingleLineContent(
                orderer,
                (Void) -> name(),
                50);
    }

    // Used by Attachment
    public VPropertyBase(Class<T> valueClass, String contentLine)
    {
        this();
        this.valueClass = valueClass;
        setConverterByClass(valueClass);
        parseContent(contentLine);
    }

    // copy constructor
    public VPropertyBase(VPropertyBase<T,U> source)
    {
    	this();
        setConverter(source.getConverter());
        T valueCopy = copyValue(source.getValue());
        setValue(valueCopy);
        source.copyChildrenInto(this);
        setParent(source.getParent());
    }
    
    // constructor with only value
    public VPropertyBase(T value)
    {
        this();
        setValue(value);
    }

    // return a copy of the value
    protected T copyValue(T source)
    {
        return source; // for mutable values override in subclasses
    }
    
    // Set converter when using constructor with class parameter
    protected void setConverterByClass(Class<T> valueClass)
    {
        // do nothing - hook to override in subclass for functionality
    }
    
    /**
     * Handle non-standard property name
     */
    @Override
	protected List<Message> parseContent(String unfoldedContent)
    {
    	List<Message> messages = new ArrayList<>();
    	String propertyName = elementName(unfoldedContent);
    	boolean isNameless = propertyName == null;
    	if (isNameless)
    	{
    		unfoldedContent = ICalendarUtilities.PROPERTY_VALUE_KEY + unfoldedContent; // add delimiter to designate content as value
    	} else if (propertyName.startsWith(VPropertyElement.NON_STANDARD_PROPERTY.toString()))
        {
            ((NonStandardProperty) this).setPropertyName(propertyName);
        }
    	ICalendarUtilities.parseInlineElementsToListPair(unfoldedContent)
	        .stream()
//	        .peek(System.out::println)
	        .forEach(entry -> processInLineChild(messages, entry.getKey(), entry.getValue(), VParameter.class));

    	return messages;
    }
    
	@Override
	protected void processInLineChild(
			List<Message> messages, 
			String childName, 
			String content,
			Class<? extends VElement> singleLineChildClass)
	{
    	if (childName == ICalendarUtilities.PROPERTY_VALUE_KEY)
    	{
    		if (content != null)
    		{
	            try {
	            	actualValueContent = content;
	            	T value = getConverter().fromString(modifiedValue());
	                if (value == null)
	                {
	                    setUnknownValue(content);
	                } else
	                {
	                    setValue(value);
	                    if (value.toString() == "UNKNOWN") // enum name indicating unknown value
	                    {
	                        setUnknownValue(content);
	                    }
	                }
	            } catch (IllegalArgumentException | DateTimeException e)
	            {
	    			Message message = new Message(this,
	    					"Invalid element:" + e.getMessage() + modifiedValue(),
	    					MessageEffect.MESSAGE_ONLY);
	    			messages.add(message);
	            }
    		}
    	} else
    	{
    		super.processInLineChild(messages, childName, content, singleLineChildClass);
    	}
	}
    
    @Override
    public List<String> errors()
    {
        List<String> errors = super.errors();
        if (getValue() == null)
        {
//            errors.add(name() + " value is null.  The property MUST have a value.");  // Google uses empty properties
        }
        
        final ValueType valueType;
        if (getValueType() != null)
        {
            valueType = getValueType().getValue();
            boolean isValueTypeOK = isValueTypeValid(valueType);
            if (! isValueTypeOK)
            {
                errors.add(name() + " value type " + getValueType().getValue() + " is not supported.  Supported types include:" +
                        allowedValueTypes.stream().map(v -> v.toString()).collect(Collectors.joining(",")));
            }
        } else
        {
            // use default valueType
            valueType = defaultValueType;
        }
        
        List<String> valueTypeErrorList = valueType.createErrorList(getValue());
        if (valueTypeErrorList != null)
        {
            errors.addAll(valueTypeErrorList);
        }
        return errors;
    }
    
    /* test if value type is valid */
    private boolean isValueTypeValid(ValueType value)
    {
        boolean isValueTypeOK = allowedValueTypes.contains(value);
        boolean isUnknownType = value.equals(ValueType.UNKNOWN);
//        boolean isNonStandardProperty = propertyType().equals(PropertyType.NON_STANDARD);
        return (isValueTypeOK || isUnknownType);
    }

    @Override
    public String toString()
    {
        StringBuilder builder = new StringBuilder(super.toString());
        builder.append(":" + valueContent());
        // return folded line
        return ICalendarUtilities.foldLine(builder).toString();
    }
    
    @Override // Note: can't check equality of parents - causes stack overflow
    public boolean equals(Object obj)
    {
        boolean childrenEquals = super.equals(obj);
        if (! childrenEquals) return false;
        VPropertyBase<?,?> testObj = (VPropertyBase<?,?>) obj;
        boolean valueEquals = (getValue() == null) ? testObj.getValue() == null : getValue().equals(testObj.getValue());
        if (! valueEquals) return false;
        boolean nameEquals = name().equals(testObj.name());
        return nameEquals;
    }

    @Override // Note: can't check hashCode of parents - causes stack overflow
    public int hashCode()
    {
        int hash = super.hashCode();
        final int prime = 31;
        hash = prime * hash + ((converter == null) ? 0 : converter.hashCode());
        hash = prime * hash + ((propertyName == null) ? 0 : propertyName.hashCode());
        hash = prime * hash + ((actualValueContent == null) ? 0 : actualValueContent.hashCode());
        hash = prime * hash + ((unknownValue == null) ? 0 : unknownValue.hashCode());
        hash = prime * hash + ((value == null) ? 0 : value.hashCode());
        return hash;
    }
}
