package leigh.astro.indi.client;

/*
 * #%L
 * INDI for Java Client Library
 * %%
 * Copyright (C) 2013 - 2014 indiforjava
 * %%
 * This program 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 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import leigh.astro.indi.base.INDIException;
import leigh.astro.indi.base.INDISexagesimalFormatter;
import leigh.astro.indi.protocol.DefNumber;
import leigh.astro.indi.protocol.OneElement;
import leigh.astro.indi.protocol.OneNumber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Formatter;
import java.util.Locale;

/**
 * A class representing a INDI Number Element.
 *
 * @author S. Alonso (Zerjillo) [zerjioi at ugr.es]
 * @author Alex Leigh
 */
public class INDINumberElement extends INDIElement {

    /**
     * A logger for the errors.
     */
    private static final Logger LOG = LoggerFactory.getLogger(INDINumberElement.class);

    /**
     * The current value of this Number Element.
     */
    private double value;

    /**
     * The current desired value for the Element.
     */
    private Double desiredValue;

    /**
     * The number format of this Number Element.
     */
    private String numberFormat;

    /**
     * The step for this Number Element.
     */
    private double step;

    /**
     * A formatter used to parse and format the values.
     */
    private INDISexagesimalFormatter sFormatter;

    /**
     * A UI component that can be used in graphical interfaces for this Number
     * Element.
     */
    private INDIElementListener uiComponent;

    /**
     * Constructs an instance of <code>INDINumberElement</code>. Usually called
     * from a <code>INDIProperty</code>.
     *
     * @param xml      A XML Element <code>&lt;defNumber&gt;</code> describing the
     *                 Number Element.
     * @param property The <code>INDIProperty</code> to which the Element belongs.
     */
    protected INDINumberElement(DefNumber xml, INDINumberProperty property) {
        super(xml, property);
        desiredValue = null;
        setNumberFormat(xml.getFormat());
        setStep(xml.getStep());
        setValue(xml.getTextContent());
    }

    /**
     * Set the number format for this Number Element.
     *
     * @param newNumberFormat The new number format.
     */
    private void setNumberFormat(String newNumberFormat) {
        if (newNumberFormat.endsWith("m")) {
            sFormatter = new INDISexagesimalFormatter(newNumberFormat);
        }

        // Correct for Java format strings.
        if (newNumberFormat.equals("%0.f") || newNumberFormat.equals("%.f")) {
            newNumberFormat = "%.0f";
        }

        numberFormat = newNumberFormat;
    }

    /**
     * Gets the number format of this Number Element.
     *
     * @return the number format of this Number Element.
     */
    public String getNumberFormat() {
        return numberFormat;
    }

    /**
     * Gets the step for this Number Element.
     *
     * @return The step for this Number Element.
     */
    public double getStep() {
        return step;
    }

    /**
     * Gets the step for this Number Element formated as a String according to
     * the number format.
     *
     * @return The step for this Number Element formatted as a String.
     */
    public String getStepAsString() {
        return getNumberAsString(getStep());
    }

    /**
     * Gets the value of this Number Element formated as a String according to
     * the number format.
     *
     * @return The value of this Number Element formatted as a String.
     */
    @Override
    public String getValueAsString() {
        return getNumberAsString(getValue());
    }

    /**
     * Returns a number formatted according to the Number Format of this Number
     * Element.
     *
     * @param number the number to be formatted.
     * @return the number formatted according to the Number Format of this
     * Number Element.
     */
    private String getNumberAsString(double number) {
        String aux;

        if (numberFormat.endsWith("m")) {
            aux = sFormatter.format(number);
        } else {
            Formatter formatter = new Formatter(Locale.US);
            LOG.debug("Attempting to format: [format: {}] [num: {}]", getNumberFormat(), number);
            aux = formatter.format(getNumberFormat(), number).toString();
            formatter.close();
        }

        return aux;
    }

    @Override
    public Double getValue() {
        return value;
    }

    /**
     * Sets the current value of this Number Element. It is assummed that the
     * XML Element is really describing the new value for this particular Number
     * Element.
     * <p>
     * This method will notify the change of the value to the listeners.
     * <p>
     * Throws IllegalArgumentException if the <code>xml</code> is not well
     * formed (the value is not a correct number or it is not in the [min, max]
     * range).
     *
     * @param xml A XML Element &lt;oneNumber&gt; describing the Element.
     */
    @Override
    protected void setValue(OneElement<?> xml) {
        setValue(xml.getTextContent());
        notifyListeners();
    }

    /**
     * Sets the step of this Number Element.
     *
     * @param stepS A String with the step value of this Number Element
     */
    private void setStep(String stepS) {
        step = parseNumber(stepS);
    }

    /**
     * Sets the value of this Number Element.
     *
     * @param valueS A String with the new value of this Number Element
     */
    private void setValue(String valueS) {
        value = parseNumber(valueS);
    }

    /**
     * Parses a number according to the Number Format of this Number Element.
     * Throws IllegalArgumentException if the <code>number</code> is not
     * correctly formatted.
     *
     * @param number The number to be parsed.
     * @return the parsed number
     */
    private double parseNumber(String number) {
        double res;

        if (numberFormat.endsWith("m")) {
            res = sFormatter.parseSexagesimal(number);
        } else {
            try {
                res = Double.parseDouble(number);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Number value not correct");
            }
        }

        return res;
    }

    @Override
    public INDIElementListener getDefaultUIComponent() throws INDIException {
        if (uiComponent != null) {
            removeINDIElementListener(uiComponent);
        }
        uiComponent = INDIViewCreator.getDefault().createNumberElementView(this, getProperty().getPermission());
        addINDIElementListener(uiComponent);

        return uiComponent;
    }

    /**
     * Checks if a desired valueToCheck would be correct to be applied to the
     * Number Element.
     *
     * @param valueToCheck The valueToCheck to be checked (usually a String, but can be a
     *                     Double).
     * @return <code>true</code> if the <code>valueToCheck</code> is a valid
     * Double or a correct String according to the Number Format.
     * <code>false</code> otherwise.
     * @throws INDIValueException if <code>valueToCheck</code> is <code>null</code> or if it is
     *                            not a Double or a correctly formatted <code>String</code>.
     */
    @Override
    public boolean checkCorrectValue(Object valueToCheck) throws INDIValueException {
        if (valueToCheck == null) {
            throw new INDIValueException(this, "null value");
        }

        double d;

        if (valueToCheck instanceof Double) {
            d = ((Double) valueToCheck).doubleValue();
        } else {
            if (valueToCheck instanceof String) {
                String val = (String) valueToCheck;
                try {
                    d = parseNumber(val);
                } catch (IllegalArgumentException e) {
                    throw new INDIValueException(this, e.getMessage());
                }
            } else {
                throw new INDIValueException(this, "The number value is not correct (not Double nor String)");
            }
        }

        return true;
    }

    @Override
    public String getNameAndValueAsString() {
        return getName() + " - " + this.getValueAsString();
    }

    @Override
    public Double getDesiredValue() {
        return desiredValue;
    }

    @Override
    public void setDesiredValue(Object desiredValue) throws INDIValueException {
        if (desiredValue instanceof String) {
            setDesiredValueAsString((String) desiredValue);
        } else if (desiredValue instanceof Double) {
            setDesiredValueAsdouble(((Double) desiredValue).doubleValue());
        } else {
            throw new INDIValueException(this, "Value for a Number Element must be a String or a Double");
        }
    }

    /**
     * Sets the desired value from a String.
     *
     * @param newDesiredValue The new desired Value
     * @throws INDIValueException if the desired value not in range
     */
    private void setDesiredValueAsString(String newDesiredValue) throws INDIValueException {
        double dd = parseNumber(newDesiredValue);

        desiredValue = new Double(dd);
    }

    /**
     * Sets the desired value from a double.
     *
     * @param newDesiredValue The new desired Value
     * @throws INDIValueException if the desired value not in range
     */
    private void setDesiredValueAsdouble(double newDesiredValue) throws INDIValueException {
        double dd = newDesiredValue;

        desiredValue = new Double(dd);
    }

    @Override
    public boolean isChanged() {
        return desiredValue != null;
    }

    /**
     * Returns the XML code &lt;oneNumber&gt; representing this Number Element
     * with a new desired value. Resets the desired value.
     *
     * @return the XML code <code>&lt;oneNumber&gt;</code> representing this
     * Number Element with a new value.
     * @see #setDesiredValue
     */
    @Override
    protected OneNumber getXMLOneElementNewValue() {
        OneNumber xml = new OneNumber().setName(getName()).setTextContent(Double.toString(desiredValue.doubleValue()));

        desiredValue = null;

        return xml;
    }

    @Override
    public String toString() {
        return this.getNumberAsString(value).trim();
    }
}
