/*
 * Copyright (C) 2020 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.keyguard;

import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.widget.TextView;

import com.android.systemui.R;

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

import kotlin.Unit;

/**
 * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
 * The time's text color is a gradient that changes its colors based on its controller.
 */
public class AnimatableClockView extends TextView {
    private static final CharSequence DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm";
    private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
    private static final long DOZE_ANIM_DURATION = 300;
    private static final long APPEAR_ANIM_DURATION = 350;
    private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
    private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;

    private final Calendar mTime = Calendar.getInstance();

    private final int mDozingWeight;
    private final int mLockScreenWeight;
    private CharSequence mFormat;
    private CharSequence mDescFormat;
    private int mDozingColor;
    private int mLockScreenColor;
    private float mLineSpacingScale = 1f;
    private int mChargeAnimationDelay = 0;

    private TextAnimator mTextAnimator = null;
    private Runnable mOnTextAnimatorInitialized;

    private boolean mIsSingleLine;

    public AnimatableClockView(Context context) {
        this(context, null, 0, 0);
    }

    public AnimatableClockView(Context context, AttributeSet attrs) {
        this(context, attrs, 0, 0);
    }

    public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        TypedArray ta = context.obtainStyledAttributes(
                attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes);
        try {
            mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
            mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
            mChargeAnimationDelay = ta.getInt(
                    R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
        } finally {
            ta.recycle();
        }

        ta = context.obtainStyledAttributes(
                attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
        try {
            mIsSingleLine = ta.getBoolean(android.R.styleable.TextView_singleLine, false);
        } finally {
            ta.recycle();
        }

        refreshFormat();
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        refreshFormat();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }

    int getDozingWeight() {
        if (useBoldedVersion()) {
            return mDozingWeight + 100;
        }
        return mDozingWeight;
    }

    int getLockScreenWeight() {
        if (useBoldedVersion()) {
            return mLockScreenWeight + 100;
        }
        return mLockScreenWeight;
    }

    /**
     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
     */
    boolean useBoldedVersion() {
        // "Bold text" fontWeightAdjustment is 300.
        return getResources().getConfiguration().fontWeightAdjustment > 100;
    }

    void refreshTime() {
        mTime.setTimeInMillis(System.currentTimeMillis());
        setText(DateFormat.format(mFormat, mTime));
        setContentDescription(DateFormat.format(mDescFormat, mTime));
    }

    void onTimeZoneChanged(TimeZone timeZone) {
        mTime.setTimeZone(timeZone);
        refreshFormat();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTextAnimator == null) {
            mTextAnimator = new TextAnimator(
                    getLayout(),
                    () -> {
                        invalidate();
                        return Unit.INSTANCE;
                    });
            if (mOnTextAnimatorInitialized != null) {
                mOnTextAnimatorInitialized.run();
                mOnTextAnimatorInitialized = null;
            }
        } else {
            mTextAnimator.updateLayout(getLayout());
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mTextAnimator.draw(canvas);
    }

    void setLineSpacingScale(float scale) {
        mLineSpacingScale = scale;
        setLineSpacing(0, mLineSpacingScale);
    }

    void setColors(int dozingColor, int lockScreenColor) {
        mDozingColor = dozingColor;
        mLockScreenColor = lockScreenColor;
    }

    void animateAppearOnLockscreen() {
        if (mTextAnimator == null) {
            return;
        }

        setTextStyle(
                getDozingWeight(),
                -1 /* text size, no update */,
                mLockScreenColor,
                false /* animate */,
                0 /* duration */,
                0 /* delay */,
                null /* onAnimationEnd */);

        setTextStyle(
                getLockScreenWeight(),
                -1 /* text size, no update */,
                mLockScreenColor,
                true, /* animate */
                APPEAR_ANIM_DURATION,
                0 /* delay */,
                null /* onAnimationEnd */);
    }

    void animateCharge(DozeStateGetter dozeStateGetter) {
        if (mTextAnimator == null || mTextAnimator.isRunning()) {
            // Skip charge animation if dozing animation is already playing.
            return;
        }
        Runnable startAnimPhase2 = () -> setTextStyle(
                dozeStateGetter.isDozing() ? getDozingWeight() : getLockScreenWeight() /* weight */,
                -1,
                null,
                true /* animate */,
                CHARGE_ANIM_DURATION_PHASE_1,
                0 /* delay */,
                null /* onAnimationEnd */);
        setTextStyle(dozeStateGetter.isDozing()
                        ? getLockScreenWeight()
                        : getDozingWeight()/* weight */,
                -1,
                null,
                true /* animate */,
                CHARGE_ANIM_DURATION_PHASE_0,
                mChargeAnimationDelay,
                startAnimPhase2);
    }

    void animateDoze(boolean isDozing, boolean animate) {
        setTextStyle(isDozing ? getDozingWeight() : getLockScreenWeight() /* weight */,
                -1,
                isDozing ? mDozingColor : mLockScreenColor,
                animate,
                DOZE_ANIM_DURATION,
                0 /* delay */,
                null /* onAnimationEnd */);
    }

    /**
     * Set text style with an optional animation.
     *
     * By passing -1 to weight, the view preserves its current weight.
     * By passing -1 to textSize, the view preserves its current text size.
     *
     * @param weight text weight.
     * @param textSize font size.
     * @param animate true to animate the text style change, otherwise false.
     */
    private void setTextStyle(
            @IntRange(from = 0, to = 1000) int weight,
            @FloatRange(from = 0) float textSize,
            Integer color,
            boolean animate,
            long duration,
            long delay,
            Runnable onAnimationEnd) {
        if (mTextAnimator != null) {
            mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null,
                    delay, onAnimationEnd);
        } else {
            // when the text animator is set, update its start values
            mOnTextAnimatorInitialized =
                    () -> mTextAnimator.setTextStyle(
                            weight, textSize, color, false, duration, null,
                            delay, onAnimationEnd);
        }
    }

    void refreshFormat() {
        Patterns.update(mContext);

        final boolean use24HourFormat = DateFormat.is24HourFormat(getContext());
        if (mIsSingleLine && use24HourFormat) {
            mFormat = Patterns.sClockView24;
        } else if (!mIsSingleLine && use24HourFormat) {
            mFormat = DOUBLE_LINE_FORMAT_24_HOUR;
        } else if (mIsSingleLine && !use24HourFormat) {
            mFormat = Patterns.sClockView12;
        } else {
            mFormat = DOUBLE_LINE_FORMAT_12_HOUR;
        }

        mDescFormat = use24HourFormat ? Patterns.sClockView24 : Patterns.sClockView12;
        refreshTime();
    }

    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
    // This is an optimization to ensure we only recompute the patterns when the inputs change.
    private static final class Patterns {
        static String sClockView12;
        static String sClockView24;
        static String sCacheKey;

        static void update(Context context) {
            final Locale locale = Locale.getDefault();
            final Resources res = context.getResources();
            final String clockView12Skel = res.getString(R.string.clock_12hr_format);
            final String clockView24Skel = res.getString(R.string.clock_24hr_format);
            final String key = locale.toString() + clockView12Skel + clockView24Skel;
            if (key.equals(sCacheKey)) return;
            sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel);

            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
            // format.  The following code removes the AM/PM indicator if we didn't want it.
            if (!clockView12Skel.contains("a")) {
                sClockView12 = sClockView12.replaceAll("a", "").trim();
            }
            sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel);
            sCacheKey = key;
        }
    }

    interface DozeStateGetter {
        boolean isDozing();
    }
}
