/*
 * Copyright (C) 2015 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.deskclock.uidata;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;

import com.android.deskclock.AlarmClockFragment;
import com.android.deskclock.ClockFragment;
import com.android.deskclock.R;
import com.android.deskclock.stopwatch.StopwatchFragment;
import com.android.deskclock.timer.TimerFragment;

import java.util.Calendar;

import static com.android.deskclock.Utils.enforceMainLooper;

/**
 * All application-wide user interface data is accessible through this singleton.
 */
public final class UiDataModel {

    /** Identifies each of the primary tabs within the application. */
    public enum Tab {
        ALARMS(AlarmClockFragment.class, R.drawable.ic_tab_alarm, R.string.menu_alarm),
        CLOCKS(ClockFragment.class, R.drawable.ic_tab_clock, R.string.menu_clock),
        TIMERS(TimerFragment.class, R.drawable.ic_tab_timer, R.string.menu_timer),
        STOPWATCH(StopwatchFragment.class, R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);

        private final String mFragmentClassName;
        private final @DrawableRes int mIconResId;
        private final @StringRes int mLabelResId;

        Tab(Class fragmentClass, @DrawableRes int iconResId, @StringRes int labelResId) {
            mFragmentClassName = fragmentClass.getName();
            mIconResId = iconResId;
            mLabelResId = labelResId;
        }

        public String getFragmentClassName() { return mFragmentClassName; }
        public @DrawableRes int getIconResId() { return mIconResId; }
        public @StringRes int getLabelResId() { return mLabelResId; }
    }

    /** The single instance of this data model that exists for the life of the application. */
    private static final UiDataModel sUiDataModel = new UiDataModel();

    public static UiDataModel getUiDataModel() {
        return sUiDataModel;
    }

    private Context mContext;

    /** The model from which tab data are fetched. */
    private TabModel mTabModel;

    /** The model from which formatted strings are fetched. */
    private FormattedStringModel mFormattedStringModel;

    /** The model from which timed callbacks originate. */
    private PeriodicCallbackModel mPeriodicCallbackModel;

    private UiDataModel() {}

    /**
     * The context may be set precisely once during the application life.
     */
    public void init(Context context, SharedPreferences prefs) {
        if (mContext != context) {
            mContext = context.getApplicationContext();

            mPeriodicCallbackModel = new PeriodicCallbackModel(mContext);
            mFormattedStringModel = new FormattedStringModel(mContext);
            mTabModel = new TabModel(prefs);
        }
    }

    /**
     * To display the alarm clock in this font, use the character {@link R.string#clock_emoji}.
     *
     * @return a special font containing a glyph that draws an alarm clock
     */
    public Typeface getAlarmIconTypeface() {
        return Typeface.createFromAsset(mContext.getAssets(), "fonts/clock.ttf");
    }

    //
    // Formatted Strings
    //

    /**
     * This method is intended to be used when formatting numbers occurs in a hotspot such as the
     * update loop of a timer or stopwatch. It returns cached results when possible in order to
     * provide speed and limit garbage to be collected by the virtual machine.
     *
     * @param value a positive integer to format as a String
     * @return the {@code value} formatted as a String in the current locale
     * @throws IllegalArgumentException if {@code value} is negative
     */
    public String getFormattedNumber(int value) {
        enforceMainLooper();
        return mFormattedStringModel.getFormattedNumber(value);
    }

    /**
     * This method is intended to be used when formatting numbers occurs in a hotspot such as the
     * update loop of a timer or stopwatch. It returns cached results when possible in order to
     * provide speed and limit garbage to be collected by the virtual machine.
     *
     * @param value a positive integer to format as a String
     * @param length the length of the String; zeroes are padded to match this length
     * @return the {@code value} formatted as a String in the current locale and padded to the
     *      requested {@code length}
     * @throws IllegalArgumentException if {@code value} is negative
     */
    public String getFormattedNumber(int value, int length) {
        enforceMainLooper();
        return mFormattedStringModel.getFormattedNumber(value, length);
    }

    /**
     * This method is intended to be used when formatting numbers occurs in a hotspot such as the
     * update loop of a timer or stopwatch. It returns cached results when possible in order to
     * provide speed and limit garbage to be collected by the virtual machine.
     *
     * @param negative force a minus sign (-) onto the display, even if {@code value} is {@code 0}
     * @param value a positive integer to format as a String
     * @param length the length of the String; zeroes are padded to match this length. If
     *      {@code negative} is {@code true} the return value will contain a minus sign and a total
     *      length of {@code length + 1}.
     * @return the {@code value} formatted as a String in the current locale and padded to the
     *      requested {@code length}
     * @throws IllegalArgumentException if {@code value} is negative
     */
    public String getFormattedNumber(boolean negative, int value, int length) {
        enforceMainLooper();
        return mFormattedStringModel.getFormattedNumber(negative, value, length);
    }

    /**
     * @param calendarDay any of the following values
     *                     <ul>
     *                     <li>{@link Calendar#SUNDAY}</li>
     *                     <li>{@link Calendar#MONDAY}</li>
     *                     <li>{@link Calendar#TUESDAY}</li>
     *                     <li>{@link Calendar#WEDNESDAY}</li>
     *                     <li>{@link Calendar#THURSDAY}</li>
     *                     <li>{@link Calendar#FRIDAY}</li>
     *                     <li>{@link Calendar#SATURDAY}</li>
     *                     </ul>
     * @return single-character version of weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
     */
    public String getShortWeekday(int calendarDay) {
        enforceMainLooper();
        return mFormattedStringModel.getShortWeekday(calendarDay);
    }

    /**
     * @param calendarDay any of the following values
     *                     <ul>
     *                     <li>{@link Calendar#SUNDAY}</li>
     *                     <li>{@link Calendar#MONDAY}</li>
     *                     <li>{@link Calendar#TUESDAY}</li>
     *                     <li>{@link Calendar#WEDNESDAY}</li>
     *                     <li>{@link Calendar#THURSDAY}</li>
     *                     <li>{@link Calendar#FRIDAY}</li>
     *                     <li>{@link Calendar#SATURDAY}</li>
     *                     </ul>
     * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
     */
    public String getLongWeekday(int calendarDay) {
        enforceMainLooper();
        return mFormattedStringModel.getLongWeekday(calendarDay);
    }

    //
    // Animations
    //

    /**
     * @return the duration in milliseconds of short animations
     */
    public long getShortAnimationDuration() {
        enforceMainLooper();
        return mContext.getResources().getInteger(android.R.integer.config_shortAnimTime);
    }

    /**
     * @return the duration in milliseconds of long animations
     */
    public long getLongAnimationDuration() {
        enforceMainLooper();
        return mContext.getResources().getInteger(android.R.integer.config_longAnimTime);
    }

    //
    // Tabs
    //

    /**
     * @param tabListener to be notified when the selected tab changes
     */
    public void addTabListener(TabListener tabListener) {
        enforceMainLooper();
        mTabModel.addTabListener(tabListener);
    }

    /**
     * @param tabListener to no longer be notified when the selected tab changes
     */
    public void removeTabListener(TabListener tabListener) {
        enforceMainLooper();
        mTabModel.removeTabListener(tabListener);
    }

    /**
     * @return the number of tabs
     */
    public int getTabCount() {
        enforceMainLooper();
        return mTabModel.getTabCount();
    }

    /**
     * @param ordinal the ordinal of the tab
     * @return the tab at the given {@code ordinal}
     */
    public Tab getTab(int ordinal) {
        enforceMainLooper();
        return mTabModel.getTab(ordinal);
    }

    /**
     * @param position the position of the tab in the user interface
     * @return the tab at the given {@code ordinal}
     */
    public Tab getTabAt(int position) {
        enforceMainLooper();
        return mTabModel.getTabAt(position);
    }

    /**
     * @return an enumerated value indicating the currently selected primary tab
     */
    public Tab getSelectedTab() {
        enforceMainLooper();
        return mTabModel.getSelectedTab();
    }

    /**
     * @param tab an enumerated value indicating the newly selected primary tab
     */
    public void setSelectedTab(Tab tab) {
        enforceMainLooper();
        mTabModel.setSelectedTab(tab);
    }

    /**
     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
     */
    public void addTabScrollListener(TabScrollListener tabScrollListener) {
        enforceMainLooper();
        mTabModel.addTabScrollListener(tabScrollListener);
    }

    /**
     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
     */
    public void removeTabScrollListener(TabScrollListener tabScrollListener) {
        enforceMainLooper();
        mTabModel.removeTabScrollListener(tabScrollListener);
    }

    /**
     * Updates the scrolling state in the {@link UiDataModel} for this tab.
     *
     * @param tab an enumerated value indicating the tab reporting its vertical scroll position
     * @param scrolledToTop {@code true} iff the vertical scroll position of the tab is at the top
     */
    public void setTabScrolledToTop(Tab tab, boolean scrolledToTop) {
        enforceMainLooper();
        mTabModel.setTabScrolledToTop(tab, scrolledToTop);
    }

    /**
     * @return {@code true} iff the content in the selected tab is currently scrolled to the top
     */
    public boolean isSelectedTabScrolledToTop() {
        enforceMainLooper();
        return mTabModel.isTabScrolledToTop(getSelectedTab());
    }

    //
    // Shortcut Ids
    //

    /**
     * @param category which category of shortcut of which to get the id
     * @param action the desired action to perform
     * @return the id of the shortcut
     */
    public String getShortcutId(@StringRes int category, @StringRes int action) {
        if (category == R.string.category_stopwatch) {
            return mContext.getString(category);
        }
        return mContext.getString(category) + "_" + mContext.getString(action);
    }

    //
    // Timed Callbacks
    //

    /**
     * @param runnable to be called every minute
     * @param offset an offset applied to the minute to control when the callback occurs
     */
    public void addMinuteCallback(Runnable runnable, long offset) {
        enforceMainLooper();
        mPeriodicCallbackModel.addMinuteCallback(runnable, offset);
    }

    /**
     * @param runnable to be called every quarter-hour
     * @param offset an offset applied to the quarter-hour to control when the callback occurs
     */
    public void addQuarterHourCallback(Runnable runnable, long offset) {
        enforceMainLooper();
        mPeriodicCallbackModel.addQuarterHourCallback(runnable, offset);
    }

    /**
     * @param runnable to be called every hour
     * @param offset an offset applied to the hour to control when the callback occurs
     */
    public void addHourCallback(Runnable runnable, long offset) {
        enforceMainLooper();
        mPeriodicCallbackModel.addHourCallback(runnable, offset);
    }

    /**
     * @param runnable to be called every midnight
     * @param offset an offset applied to the midnight to control when the callback occurs
     */
    public void addMidnightCallback(Runnable runnable, long offset) {
        enforceMainLooper();
        mPeriodicCallbackModel.addMidnightCallback(runnable, offset);
    }

    /**
     * @param runnable to no longer be called periodically
     */
    public void removePeriodicCallback(Runnable runnable) {
        enforceMainLooper();
        mPeriodicCallbackModel.removePeriodicCallback(runnable);
    }
}
