/*
 * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.scenario.animation.shared;

import com.sun.javafx.animation.KeyValueHelper;
import javafx.animation.Interpolator;
import javafx.animation.KeyValue;
import javafx.beans.value.WritableBooleanValue;
import javafx.beans.value.WritableDoubleValue;
import javafx.beans.value.WritableFloatValue;
import javafx.beans.value.WritableIntegerValue;
import javafx.beans.value.WritableLongValue;
import javafx.beans.value.WritableValue;

import com.sun.scenario.animation.NumberTangentInterpolator;

public abstract class InterpolationInterval {

    protected final long ticks;
    protected final Interpolator rightInterpolator;

    protected InterpolationInterval(long ticks,
            Interpolator rightInterpolator) {
        this.ticks = ticks;
        this.rightInterpolator = rightInterpolator;
    }

    public abstract void interpolate(double frac);

    public abstract void recalculateStartValue();

    public static InterpolationInterval create(KeyValue rightKeyValue,
            long ticks, KeyValue leftKeyValue, long duration) {
        switch (KeyValueHelper.getType(rightKeyValue)) {
            case BOOLEAN:
                return new BooleanInterpolationInterval(rightKeyValue, ticks,
                        leftKeyValue.getEndValue());
            case DOUBLE:
                return ((leftKeyValue.getInterpolator() instanceof NumberTangentInterpolator) || (rightKeyValue
                        .getInterpolator() instanceof NumberTangentInterpolator)) ? new TangentDoubleInterpolationInterval(
                        rightKeyValue, ticks, leftKeyValue, duration)
                        : new DoubleInterpolationInterval(rightKeyValue,
                                ticks, leftKeyValue.getEndValue());
            case FLOAT:
                return ((leftKeyValue.getInterpolator() instanceof NumberTangentInterpolator) || (rightKeyValue
                        .getInterpolator() instanceof NumberTangentInterpolator)) ? new TangentFloatInterpolationInterval(
                        rightKeyValue, ticks, leftKeyValue, duration)
                        : new FloatInterpolationInterval(rightKeyValue, ticks,
                                leftKeyValue.getEndValue());
            case INTEGER:
                return ((leftKeyValue.getInterpolator() instanceof NumberTangentInterpolator) || (rightKeyValue
                        .getInterpolator() instanceof NumberTangentInterpolator)) ? new TangentIntegerInterpolationInterval(
                        rightKeyValue, ticks, leftKeyValue, duration)
                        : new IntegerInterpolationInterval(rightKeyValue,
                                ticks, leftKeyValue.getEndValue());
            case LONG:
                return ((leftKeyValue.getInterpolator() instanceof NumberTangentInterpolator) || (rightKeyValue
                        .getInterpolator() instanceof NumberTangentInterpolator)) ? new TangentLongInterpolationInterval(
                        rightKeyValue, ticks, leftKeyValue, duration)
                        : new LongInterpolationInterval(rightKeyValue, ticks,
                                leftKeyValue.getEndValue());
            case OBJECT:
                return new ObjectInterpolationInterval(rightKeyValue, ticks,
                        leftKeyValue.getEndValue());
        }
        throw new RuntimeException("Should not reach here");
    }

    public static InterpolationInterval create(KeyValue rightKeyValue,
            long ticks) {
        switch (KeyValueHelper.getType(rightKeyValue)) {
            case BOOLEAN:
                return new BooleanInterpolationInterval(rightKeyValue, ticks);
            case DOUBLE:
                return (rightKeyValue.getInterpolator() instanceof NumberTangentInterpolator) ? new TangentDoubleInterpolationInterval(
                        rightKeyValue, ticks)
                        : new DoubleInterpolationInterval(rightKeyValue, ticks);
            case FLOAT:
                return (rightKeyValue.getInterpolator() instanceof NumberTangentInterpolator) ? new TangentFloatInterpolationInterval(
                        rightKeyValue, ticks)
                        : new FloatInterpolationInterval(rightKeyValue, ticks);
            case INTEGER:
                return (rightKeyValue.getInterpolator() instanceof NumberTangentInterpolator) ? new TangentIntegerInterpolationInterval(
                        rightKeyValue, ticks)
                        : new IntegerInterpolationInterval(rightKeyValue,
                                ticks);
            case LONG:
                return (rightKeyValue.getInterpolator() instanceof NumberTangentInterpolator) ? new TangentLongInterpolationInterval(
                        rightKeyValue, ticks) : new LongInterpolationInterval(
                        rightKeyValue, ticks);
            case OBJECT:
                return new ObjectInterpolationInterval(rightKeyValue, ticks);
        }
        throw new RuntimeException("Should not reach here");
    }

    private static abstract class TangentInterpolationInterval extends
            InterpolationInterval {

        private final double duration;
        private final double p2;
        protected final double p3;
        private final NumberTangentInterpolator leftInterpolator;

        protected double p0;
        private double p1;

        private TangentInterpolationInterval(KeyValue rightKeyValue,
                long ticks, KeyValue leftKeyValue, long duration) {
            super(ticks, rightKeyValue.getInterpolator());
            assert (rightKeyValue.getEndValue() instanceof Number)
                    && (leftKeyValue.getEndValue() instanceof Number);

            this.duration = duration;
            final Interpolator rawLeftInterpolator = leftKeyValue
                    .getInterpolator();
            leftInterpolator = (rawLeftInterpolator instanceof NumberTangentInterpolator) ? (NumberTangentInterpolator) rawLeftInterpolator
                    : null;
            recalculateStartValue(((Number) leftKeyValue.getEndValue())
                    .doubleValue());

            final NumberTangentInterpolator interpolator = (rightInterpolator instanceof NumberTangentInterpolator) ? (NumberTangentInterpolator) rightInterpolator
                    : null;
            p3 = ((Number) rightKeyValue.getEndValue()).doubleValue();
            final double p2Delta = (interpolator == null) ? 0 : (interpolator
                    .getInValue() - p3)
                    * duration
                    / interpolator.getInTicks()
                    / 3;
            p2 = p3 + p2Delta;
        }

        private TangentInterpolationInterval(KeyValue rightKeyValue,
                long ticks) {
            super(ticks, rightKeyValue.getInterpolator());
            assert rightKeyValue.getEndValue() instanceof Number;

            this.duration = ticks;
            leftInterpolator = null;

            final NumberTangentInterpolator interpolator = (rightInterpolator instanceof NumberTangentInterpolator) ? (NumberTangentInterpolator) rightInterpolator
                    : null;
            p3 = ((Number) rightKeyValue.getEndValue()).doubleValue();
            final double p2Delta = (interpolator == null) ? 0 : (interpolator
                    .getInValue() - p3)
                    * duration
                    / interpolator.getInTicks()
                    / 3;
            p2 = p3 + p2Delta;
        }

        protected double calculate(double t) {
            final double oneMinusT = 1.0 - t;
            final double tSquared = t * t;
            final double oneMinusTSquared = oneMinusT * oneMinusT;

            return oneMinusTSquared * oneMinusT * p0 + 3 * oneMinusTSquared * t
                    * p1 + 3 * oneMinusT * tSquared * p2 + tSquared * t * p3;
        }

        protected final void recalculateStartValue(double leftValue) {
            p0 = leftValue;
            final double p1Delta = (leftInterpolator == null) ? 0
                    : (leftInterpolator.getOutValue() - p0) * duration
                            / leftInterpolator.getOutTicks() / 3;
            p1 = p0 + p1Delta;
        }
    }

    private static class BooleanInterpolationInterval extends
            InterpolationInterval {

        private final WritableBooleanValue target;
        private boolean leftValue;
        private final boolean rightValue;

        private BooleanInterpolationInterval(KeyValue keyValue, long ticks,
                Object leftValue) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableBooleanValue)
                    && (keyValue.getEndValue() instanceof Boolean)
                    && (leftValue instanceof Boolean);
            this.target = (WritableBooleanValue) keyValue.getTarget();
            this.rightValue = (Boolean) keyValue.getEndValue();
            this.leftValue = (Boolean) leftValue;
        }

        private BooleanInterpolationInterval(KeyValue keyValue, long ticks) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableBooleanValue)
                    && (keyValue.getEndValue() instanceof Boolean);
            this.target = (WritableBooleanValue) keyValue.getTarget();
            this.rightValue = (Boolean) keyValue.getEndValue();
            this.leftValue = target.get();
        }

        @Override
        public void interpolate(double frac) {
            final boolean value = rightInterpolator.interpolate(leftValue,
                    rightValue, frac);
            target.set(value);
        }

        @Override
        public void recalculateStartValue() {
            leftValue = target.get();
        }
    }

    private static class DoubleInterpolationInterval extends
            InterpolationInterval {

        private final WritableDoubleValue target;
        private double leftValue;
        private final double rightValue;

        private DoubleInterpolationInterval(KeyValue keyValue, long ticks,
                Object leftValue) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableDoubleValue)
                    && (keyValue.getEndValue() instanceof Number)
                    && (leftValue instanceof Number);
            this.target = (WritableDoubleValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).doubleValue();
            this.leftValue = ((Number) leftValue).doubleValue();
        }

        private DoubleInterpolationInterval(KeyValue keyValue, long ticks) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableDoubleValue)
                    && (keyValue.getEndValue() instanceof Number);
            this.target = (WritableDoubleValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).doubleValue();
            this.leftValue = target.get();
        }

        @Override
        public void interpolate(double frac) {
            final double value = rightInterpolator.interpolate(leftValue,
                    rightValue, frac);
            target.set(value);
        }

        @Override
        public void recalculateStartValue() {
            leftValue = target.get();
        }
    }

    private static class TangentDoubleInterpolationInterval extends
            TangentInterpolationInterval {

        private final WritableDoubleValue target;

        private TangentDoubleInterpolationInterval(KeyValue rightKeyValue,
                long ticks, KeyValue leftKeyValue, long duration) {
            super(rightKeyValue, ticks, leftKeyValue, duration);
            assert rightKeyValue.getTarget() instanceof WritableDoubleValue;
            this.target = (WritableDoubleValue) rightKeyValue.getTarget();
        }

        private TangentDoubleInterpolationInterval(KeyValue rightKeyValue,
                long ticks) {
            super(rightKeyValue, ticks);
            assert rightKeyValue.getTarget() instanceof WritableDoubleValue;
            this.target = (WritableDoubleValue) rightKeyValue.getTarget();
            recalculateStartValue(target.get());
        }

        @Override
        public void interpolate(double frac) {
            target.set(calculate(frac));
        }

        @Override
        public void recalculateStartValue() {
            recalculateStartValue(target.get());
        }
    }

    private static class FloatInterpolationInterval extends
            InterpolationInterval {

        private final WritableFloatValue target;
        private float leftValue;
        private final float rightValue;

        private FloatInterpolationInterval(KeyValue keyValue, long ticks,
                Object leftValue) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableFloatValue)
                    && (keyValue.getEndValue() instanceof Number)
                    && (leftValue instanceof Number);
            this.target = (WritableFloatValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).floatValue();
            this.leftValue = ((Number) leftValue).floatValue();
        }

        private FloatInterpolationInterval(KeyValue keyValue, long ticks) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableFloatValue)
                    && (keyValue.getEndValue() instanceof Number);
            this.target = (WritableFloatValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).floatValue();
            this.leftValue = target.get();
        }

        @Override
        public void interpolate(double frac) {
            final float value = (float) rightInterpolator.interpolate(
                    leftValue, rightValue, frac);
            target.set(value);
        }

        @Override
        public void recalculateStartValue() {
            leftValue = target.get();
        }
    }

    private static class TangentFloatInterpolationInterval extends
            TangentInterpolationInterval {

        private final WritableFloatValue target;

        private TangentFloatInterpolationInterval(KeyValue rightKeyValue,
                long ticks, KeyValue leftKeyValue, long duration) {
            super(rightKeyValue, ticks, leftKeyValue, duration);
            assert rightKeyValue.getTarget() instanceof WritableFloatValue;
            this.target = (WritableFloatValue) rightKeyValue.getTarget();
        }

        private TangentFloatInterpolationInterval(KeyValue rightKeyValue,
                long ticks) {
            super(rightKeyValue, ticks);
            assert rightKeyValue.getTarget() instanceof WritableFloatValue;
            this.target = (WritableFloatValue) rightKeyValue.getTarget();
            recalculateStartValue(target.get());
        }

        @Override
        public void interpolate(double frac) {
            target.set((float) calculate(frac));
        }

        @Override
        public void recalculateStartValue() {
            recalculateStartValue(target.get());
        }
    }

    private static class IntegerInterpolationInterval extends
            InterpolationInterval {

        private final WritableIntegerValue target;
        private int leftValue;
        private final int rightValue;

        private IntegerInterpolationInterval(KeyValue keyValue, long ticks,
                Object leftValue) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableIntegerValue)
                    && (keyValue.getEndValue() instanceof Number)
                    && (leftValue instanceof Number);
            this.target = (WritableIntegerValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).intValue();
            this.leftValue = ((Number) leftValue).intValue();
        }

        private IntegerInterpolationInterval(KeyValue keyValue, long ticks) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableIntegerValue)
                    && (keyValue.getEndValue() instanceof Number);
            this.target = (WritableIntegerValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).intValue();
            this.leftValue = target.get();
        }

        @Override
        public void interpolate(double frac) {
            final int value = rightInterpolator.interpolate(leftValue,
                    rightValue, frac);
            target.set(value);
        }

        @Override
        public void recalculateStartValue() {
            leftValue = target.get();
        }
    }

    private static class TangentIntegerInterpolationInterval extends
            TangentInterpolationInterval {

        private final WritableIntegerValue target;

        private TangentIntegerInterpolationInterval(KeyValue rightKeyValue,
                long ticks, KeyValue leftKeyValue, long duration) {
            super(rightKeyValue, ticks, leftKeyValue, duration);
            assert rightKeyValue.getTarget() instanceof WritableIntegerValue;
            this.target = (WritableIntegerValue) rightKeyValue.getTarget();
        }

        private TangentIntegerInterpolationInterval(KeyValue rightKeyValue,
                long ticks) {
            super(rightKeyValue, ticks);
            assert rightKeyValue.getTarget() instanceof WritableIntegerValue;
            this.target = (WritableIntegerValue) rightKeyValue.getTarget();
            recalculateStartValue(target.get());
        }

        @Override
        public void interpolate(double frac) {
            target.set((int) Math.round(calculate(frac)));
        }

        @Override
        public void recalculateStartValue() {
            recalculateStartValue(target.get());
        }
    }

    private static class LongInterpolationInterval extends
            InterpolationInterval {

        private final WritableLongValue target;
        private long leftValue;
        private final long rightValue;

        private LongInterpolationInterval(KeyValue keyValue, long ticks,
                Object leftValue) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableLongValue)
                    && (keyValue.getEndValue() instanceof Number)
                    && (leftValue instanceof Number);
            this.target = (WritableLongValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).longValue();
            this.leftValue = ((Number) leftValue).longValue();
        }

        private LongInterpolationInterval(KeyValue keyValue, long ticks) {
            super(ticks, keyValue.getInterpolator());
            assert (keyValue.getTarget() instanceof WritableLongValue)
                    && (keyValue.getEndValue() instanceof Number);
            this.target = (WritableLongValue) keyValue.getTarget();
            this.rightValue = ((Number) keyValue.getEndValue()).longValue();
            this.leftValue = target.get();
        }

        @Override
        public void interpolate(double frac) {
            final long value = rightInterpolator.interpolate(leftValue,
                    rightValue, frac);
            target.set(value);
        }

        @Override
        public void recalculateStartValue() {
            leftValue = target.get();
        }
    }

    private static class TangentLongInterpolationInterval extends
            TangentInterpolationInterval {

        private final WritableLongValue target;

        private TangentLongInterpolationInterval(KeyValue rightKeyValue,
                long ticks, KeyValue leftKeyValue, long duration) {
            super(rightKeyValue, ticks, leftKeyValue, duration);
            assert rightKeyValue.getTarget() instanceof WritableLongValue;
            this.target = (WritableLongValue) rightKeyValue.getTarget();
        }

        private TangentLongInterpolationInterval(KeyValue rightKeyValue,
                long ticks) {
            super(rightKeyValue, ticks);
            assert rightKeyValue.getTarget() instanceof WritableLongValue;
            this.target = (WritableLongValue) rightKeyValue.getTarget();
            recalculateStartValue(target.get());
        }

        @Override
        public void interpolate(double frac) {
            target.set(Math.round(calculate(frac)));
        }

        @Override
        public void recalculateStartValue() {
            recalculateStartValue(target.get());
        }
    }

    private static class ObjectInterpolationInterval extends
            InterpolationInterval {

        @SuppressWarnings("rawtypes")
        private final WritableValue target;
        private Object leftValue;
        private final Object rightValue;

        private ObjectInterpolationInterval(KeyValue keyValue, long ticks,
                Object leftValue) {
            super(ticks, keyValue.getInterpolator());
            this.target = keyValue.getTarget();
            this.rightValue = keyValue.getEndValue();
            this.leftValue = leftValue;
        }

        private ObjectInterpolationInterval(KeyValue keyValue, long ticks) {
            super(ticks, keyValue.getInterpolator());
            this.target = keyValue.getTarget();
            this.rightValue = keyValue.getEndValue();
            this.leftValue = target.getValue();
        }

        @SuppressWarnings("unchecked")
        @Override
        public void interpolate(double frac) {
            final Object value = rightInterpolator.interpolate(leftValue,
                    rightValue, frac);
            target.setValue(value);
        }

        @Override
        public void recalculateStartValue() {
            leftValue = target.getValue();
        }
    }

}