/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * 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 com.android.ddmuilib.location;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Text;

import java.text.DecimalFormat;
import java.text.ParseException;

/**
 * Encapsulation of controls handling a location coordinate in decimal and sexagesimal.
 * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener}
 * on all the {@link Text} widgets.
 * <p/>To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by
 * a call to {@link #isValueValid()})
 */
public final class CoordinateControls {
    private double mValue;
    private boolean mValueValidity = false;
    private Text mDecimalText;
    private Text mSexagesimalDegreeText;
    private Text mSexagesimalMinuteText;
    private Text mSexagesimalSecondText;
    private final DecimalFormat mDecimalFormat = new DecimalFormat();

    /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)}
     * is called. This is an int instead of a boolean to act as a counter. */
    private int mManualTextChange = 0;

    /**
     * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode.
     */
    private ModifyListener mSexagesimalListener = new ModifyListener() {
        @Override
        public void modifyText(ModifyEvent event) {
            if (mManualTextChange > 0) {
                return;
            }
            try {
                mValue = getValueFromSexagesimalControls();
                setValueIntoDecimalControl(mValue);
                mValueValidity = true;
            } catch (ParseException e) {
                // wrong format empty the decimal controls.
                mValueValidity = false;
                resetDecimalControls();
            }
        }
    };

    /**
     * Creates the {@link Text} control for the decimal display of the coordinate.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createDecimalText(Composite parent) {
        mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent event) {
                if (mManualTextChange > 0) {
                    return;
                }
                try {
                    mValue = mDecimalFormat.parse(mDecimalText.getText()).doubleValue();
                    setValueIntoSexagesimalControl(mValue);
                    mValueValidity = true;
                } catch (ParseException e) {
                    // wrong format empty the sexagesimal controls.
                    mValueValidity = false;
                    resetSexagesimalControls();
                }
            }
        });
    }

    /**
     * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal
     * mode.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createSexagesimalDegreeText(Composite parent) {
        mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$
    }

    /**
     * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal
     * mode.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createSexagesimalMinuteText(Composite parent) {
        mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$
    }

    /**
     * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal
     * mode.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createSexagesimalSecondText(Composite parent) {
        mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$
    }

    /**
     * Sets the coordinate into the {@link Text} controls.
     * @param value the coordinate value to set.
     */
    public void setValue(double value) {
        mValue = value;
        mValueValidity = true;
        setValueIntoDecimalControl(value);
        setValueIntoSexagesimalControl(value);
    }

    /**
     * Returns whether the value in the control(s) is valid.
     */
    public boolean isValueValid() {
        return mValueValidity;
    }

    /**
     * Returns the current value set in the control(s).
     * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed
     * before any call to this method.
     */
    public double getValue() {
        return mValue;
    }

    /**
     * Enables or disables all the {@link Text} controls.
     * @param enabled the enabled state.
     */
    public void setEnabled(boolean enabled) {
        mDecimalText.setEnabled(enabled);
        mSexagesimalDegreeText.setEnabled(enabled);
        mSexagesimalMinuteText.setEnabled(enabled);
        mSexagesimalSecondText.setEnabled(enabled);
    }

    private void resetDecimalControls() {
        mManualTextChange++;
        mDecimalText.setText(""); //$NON-NLS-1$
        mManualTextChange--;
    }

    private void resetSexagesimalControls() {
        mManualTextChange++;
        mSexagesimalDegreeText.setText(""); //$NON-NLS-1$
        mSexagesimalMinuteText.setText(""); //$NON-NLS-1$
        mSexagesimalSecondText.setText(""); //$NON-NLS-1$
        mManualTextChange--;
    }

    /**
     * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener}
     * @param parent the parent {@link Composite}.
     * @param defaultString the default string to be used to compute the {@link Text} control
     * size hint.
     * @param listener the {@link ModifyListener} to be called when the {@link Text} control is
     * modified.
     */
    private Text createTextControl(Composite parent, String defaultString,
            ModifyListener listener) {
        // create the control
        Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE);

        // add the standard listener to it.
        text.addModifyListener(listener);

        // compute its size/
        mManualTextChange++;
        text.setText(defaultString);
        text.pack();
        Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
        text.setText(""); //$NON-NLS-1$
        mManualTextChange--;

        GridData gridData = new GridData();
        gridData.widthHint = size.x;
        text.setLayoutData(gridData);

        return text;
    }

    private double getValueFromSexagesimalControls() throws ParseException {
        double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue();
        double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue();
        double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue();

        boolean isPositive = (degrees >= 0.);
        degrees = Math.abs(degrees);

        double value = degrees + minutes / 60. + seconds / 3600.;
        return isPositive ? value : - value;
    }

    private void setValueIntoDecimalControl(double value) {
        mManualTextChange++;
        mDecimalText.setText(String.format("%.6f", value));
        mManualTextChange--;
    }

    private void setValueIntoSexagesimalControl(double value) {
        // get the sign and make the number positive no matter what.
        boolean isPositive = (value >= 0.);
        value = Math.abs(value);

        // get the degree
        double degrees = Math.floor(value);

        // get the minutes
        double minutes = Math.floor((value - degrees) * 60.);

        // get the seconds.
        double seconds = (value - degrees) * 3600. - minutes * 60.;

        mManualTextChange++;
        mSexagesimalDegreeText.setText(
                Integer.toString(isPositive ? (int)degrees : (int)- degrees));
        mSexagesimalMinuteText.setText(Integer.toString((int)minutes));
        mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$
        mManualTextChange--;
    }
}
