/* 
 * Copyright 2012 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License ("License") as 
 * published by the Free Software Foundation  (version 3 of the License). 
 * This software 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 copy of the 
 * License you received along with this software for more details.
 */

package de.avm.android.fritzapp.gui;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

import de.avm.android.fritzapp.com.ComSettingsChecker;
import de.avm.android.fritzapp.com.DataHub;
import de.avm.android.fritzapp.com.discovery.BoxInfo;
import de.avm.android.fritzapp.R;
import de.avm.android.tr064.Tr064Boxinfo;
import de.avm.android.tr064.Tr064Capabilities;
import de.avm.android.tr064.model.TamInfo;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Handler;
import android.preference.ListPreference;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class TamPreference extends ListPreference
{
	private static final int BUSY_WIDGET = R.layout.listitem_progress;
	
	public interface OnChangedListener
	{
		void onChanged(boolean isInSettings);
	}

	public interface OnResultListener<RESULT>
	{
		/**
		 * Callback to get result from async method
		 * 
		 * @param udn box the result is related to
		 * @param result the result
		 */
		void onResult(String udn, RESULT result);
		
		/**
		 * Called if onResult() is not called
		 * 
		 * @param error
		 * 		Error if available
		 */
		void onError(Exception error);
	}

	private OnChangedListener mOnChangedListener = null;

	public void setOnChangedListener(OnChangedListener listener)
	{
		mOnChangedListener = listener;
	}

	private LayoutInflater mLayoutInflater;
	private ViewGroup mLayoutWidgetFrame = null;
	private boolean mIsBusy = false;
	private boolean mIsActive = false;
	
	private static DataHub mFritzBox = new DataHub();
	private OnResultListener<String> mLoaderTask = null;
	private boolean mTamNamesLoaded = false;
	private Handler mHandler = new Handler();

	public boolean isActive()
	{
		return mIsActive;
	}
	
	/**
	 * Enables or disables the preference with respect
	 * to its internal busy state
	 * Do not call setEnabled() directly!
	 *  
	 * @param active
	 * 			true to disable preference
	 */
	public void setActive(boolean active)
	{
		mIsActive = active;
		if (isEnabled() != (active && !mIsBusy))
			setEnabled(active && !mIsBusy);
	}
	
	/**
	 * Creates new instance
	 * 
	 * @param context
	 * 			the context the preference will be shown
	 * @param load
	 * 			true to reload TAM info from box, otherwise use
	 * 			cached info if available
	 */
	public TamPreference(Context context, boolean load)
	{
		super(context);

		mLayoutInflater = (LayoutInflater)context
        		.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		addInstance(this);
		
		if (!load)
		{
			synchronized(mCachedInfo)
			{
				if (mCachedInfo.isValid())
				{
					super.setEntries(mCachedInfo.getTamEntries(context));
					super.setEntryValues(mCachedInfo.getTamValues(context));
					setSummary(mCachedInfo.mSelectedTam);
					mTamNamesLoaded = true;
					selectCurrent();
					return;
				}
			}
		}
		load();
	}
	
	/**
	 * begins loading TAM info from box 
	 * (to be called from UI thread)
	 */
	private void load()
	{
		if (mIsBusy) return;
		
		Tr064Capabilities capabilities = ComSettingsChecker.getTr064Capabilities();
		if ((capabilities == null) ||
				!capabilities.has(Tr064Capabilities.Capability.TAM))
		{
			// no TAM, this preference should not be shown anyway, set default 
			super.setEntryValues(CachedInfo.DEFAULT_ENTRIES);
			super.setEntries(CachedInfo.DEFAULT_ENTRIES);
			selectCurrent();
			return;
		}
		
		// start loading TAMs
		OnResultListener<String> loaderTask = new OnResultListener<String>()
		{
			public void onResult(String udn, String result)
			{
				final OnResultListener<String> me = this; 
				mHandler.post(new Runnable()
				{
					public void run()
					{
						if (me == mLoaderTask)
						{
							synchronized(mCachedInfo)
							{
								TamPreference.super.setEntryValues(mCachedInfo
										.getTamValues(getContext()));
								TamPreference.super.setEntries(mCachedInfo
										.getTamEntries(getContext()));
							}

							mLoaderTask = null;
							setBusy(false);
							mTamNamesLoaded = true;
							selectCurrent();
						}
					}
				});
			}

			public void onError(Exception error)
			{
				final OnResultListener<String> me = this; 
				mHandler.post(new Runnable()
				{
					public void run()
					{
						if (me == mLoaderTask)
						{
							mLoaderTask = null;
							setBusy(false);
							
							// error loading info from box, so use fixed TAM list
							TamPreference.super.setEntryValues(CachedInfo.DEFAULT_ENTRIES);
							TamPreference.super.setEntries(CachedInfo.DEFAULT_ENTRIES);
							selectCurrent();
						}
					}
				});
			}
		};
		setBusy(true);
		mLoaderTask = loaderTask;
		String tam = getSelectedTam(getContext(), loaderTask);
		if (tam == null)
		{
			// failed to start loading
			mLoaderTask = null;
			setBusy(false);
		}
		else if (!TextUtils.isEmpty(tam)) setSummary(tam);
	}

	@Override
    protected View onCreateView(ViewGroup parent)
	{
        final View layout = mLayoutInflater.inflate(getLayoutResource(), parent, false); 
        
    	// remember widget view for setBusy()
    	mLayoutWidgetFrame = (ViewGroup)layout.findViewById(android.R.id.widget_frame);
    	if (mLayoutWidgetFrame != null)
    	{
	    	// inflate BUSY_WIDGET if setBusy() already called
	    	int resId = (mIsBusy) ? BUSY_WIDGET : getWidgetLayoutResource();
	    	if (resId != 0)
	    		mLayoutInflater.inflate(resId, mLayoutWidgetFrame);
    	}

		return layout;
    }
	
	private void selectCurrent()
	{
		int sel = -1;
		BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		if (boxInfo != null)
		{
			String current = null;
			synchronized(mCachedInfo)
			{
				if (mCachedInfo.isBox(boxInfo.getUdn()))
					current = mCachedInfo.mSelectedTam;
			}
			if (!TextUtils.isEmpty(current))
			{
				CharSequence[] tamArray = getEntryValues();
				for (int ii = 0; ii < tamArray.length; ii++)
					if (tamArray[ii].equals(current))
					{
						sel = ii;
						break;
					}
			}
		}
		setValueIndex((sel < 0) ? 0 : sel);
	}

	/**
	 * Enables or disables the preference and changes
	 * preference's specific widget to infinite progress
	 * symbol or reverts it
	 *  
	 * @param busy
	 * 			true to temporarily disable preference
	 */
	private void setBusy(boolean busy)
	{
		if (busy != mIsBusy)
		{
			mIsBusy = busy; 
			setEnabled(mIsActive && !busy);

	        if (mLayoutWidgetFrame != null)
	        {
		        mLayoutWidgetFrame.removeAllViews();
		    	int resId = (busy) ? BUSY_WIDGET : getWidgetLayoutResource();
		    	if (resId != 0)
			        mLayoutInflater.inflate(resId, mLayoutWidgetFrame);
		    	mLayoutWidgetFrame.postInvalidate();
	        }
		}
	}
	
	public boolean isTamNamesLoaded()
	{
		return mTamNamesLoaded;
	}
	
	@Override
	public void setEntryValues(int resId)
	{
        throw new IllegalStateException(
                "TamPreference does not allow to set entryValues array.");
	}
	
	@Override
	public void setEntryValues(CharSequence[] entryValues)
	{
        throw new IllegalStateException(
                "TamPreference does not allow to set entryValues array.");
	}
	
	@Override
	public void setEntries(int resId)
	{
        throw new IllegalStateException(
                "TamPreference does not allow to set entries array.");
	}
	
	@Override
	public void setEntries(CharSequence[] entries)
	{
        throw new IllegalStateException(
                "TamPreference does not allow to set entries array.");
	}
	
	@Override
	protected void onPrepareDialogBuilder(AlertDialog.Builder builder)
	{
		super.onPrepareDialogBuilder(builder);
	}

	@Override
	protected void onDialogClosed(boolean positiveResult)
	{
		super.onDialogClosed(positiveResult);
		
		if (positiveResult)
		{
			// save new selection
			BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
			if (boxInfo != null)
			{
				boxInfo.setTam(getValue());
				ComSettingsChecker.SaveBoxInfo(getContext());
				boolean changed = false;
				synchronized(mCachedInfo)
				{
					if (mCachedInfo.isBox(boxInfo.getUdn()))
					{
						mCachedInfo.mSelectedTam = getValue();
						changed = true;
					}
				}
				if (changed) updateCachedInfo(getContext(), boxInfo, null);
			}
		}
	}
	
	private void onChanged()
	{
		mHandler.post(new Runnable()
		{
			public void run()
			{
				boolean isInSettings = false;
				synchronized(mCachedInfo)
				{
					if (mCachedInfo.isValid())
					{
						isInSettings = mCachedInfo.mIsInSettings;
						setSummary(mCachedInfo.mSelectedTam);
						TamPreference.super.setEntries(mCachedInfo
								.getTamEntries(getContext()));
						TamPreference.super.setEntryValues(mCachedInfo
								.getTamValues(getContext()));
					}
					else
					{
						setSummary("");
						TamPreference.super.setEntries(CachedInfo.DEFAULT_ENTRIES);
						TamPreference.super.setEntryValues(CachedInfo.DEFAULT_ENTRIES);
					}
				}

				if (mOnChangedListener != null)
					mOnChangedListener.onChanged(isInSettings);
			}
		});
	}

	/*
	 * static part - loaded data are stored here
	 */

	// cached infos
	private static class CachedInfo
	{
		public static final CharSequence[] DEFAULT_ENTRIES =
				new CharSequence[] { BoxInfo.TAM_DEFAULT };
		
		public String mUdn = null;
		public TamInfo[] mTamInfos = null;
		public String mSelectedTam = null;
		public boolean mIsInSettings = false;
		
		boolean isValid()
		{
			return !TextUtils.isEmpty(mUdn);
		}
		
		boolean isBox(String udn)
		{
			return isValid() && mUdn.equals(udn);
		}
		
		CharSequence[] getTamEntries(Context context)
		{
			if ((mTamInfos != null) && (mTamInfos.length > 0))
			{
				List<CharSequence> tamEntries = new ArrayList<CharSequence>();
				String fmtTamNumber = context.getString(R.string.settings_tam_fmt); 
				for (TamInfo info : mTamInfos)
				{
					if (info.isDisplay())
					{
						if (TextUtils.isEmpty(info.getName()))
							tamEntries.add(String.format(fmtTamNumber,
									info.getIndex() + DataHub.FIRST_TAM_NUMBER_DIGIT));
						else
							tamEntries.add(info.getName());
					}
				}
				return (tamEntries.size() > 0) ?
						tamEntries.toArray(new CharSequence[0]) : DEFAULT_ENTRIES;
			}
			return DEFAULT_ENTRIES;
		}
		
		CharSequence[] getTamValues(Context context)
		{
			if ((mTamInfos != null) && (mTamInfos.length > 0))
			{
				List<CharSequence> tamValues = new ArrayList<CharSequence>();
				String fmtTamNumber = context.getString(R.string.settings_tam_fmt); 
				for (TamInfo info : mTamInfos)
				{
					if (info.isDisplay())
						tamValues.add(String.format(fmtTamNumber,
								info.getIndex() + DataHub.FIRST_TAM_NUMBER_DIGIT));
				}
				return (tamValues.size() > 0) ?
						tamValues.toArray(new CharSequence[0]) : DEFAULT_ENTRIES;
			}
			return DEFAULT_ENTRIES;
		}
	}
	private static CachedInfo mCachedInfo = new CachedInfo();

	private static ArrayList<WeakReference<TamPreference>> mInstances =
			new ArrayList<WeakReference<TamPreference>>();
	
	private void addInstance(TamPreference instance)
	{
		synchronized(mInstances)
		{
			// clean up
			ArrayList<WeakReference<TamPreference>> cleared =
					new ArrayList<WeakReference<TamPreference>>();
			for(WeakReference<TamPreference> item : mInstances)
				if (item.get() == null) cleared.add(item);
			if (cleared.size() > 0) mInstances.removeAll(cleared);
	
			// add
			if (instance != null)
				mInstances.add(new WeakReference<TamPreference>(instance));
		}
	}
	
	/**
	 * Updates cached info and notifies listeners
	 * 
	 * @param context
	 * 		context for accessing string resources
	 * @param boxInfo
	 * 		box to update cached info for
	 * @param tamInfos
	 * 		list of TAMs - null to use already cached TAMs
	 */
	public static void updateCachedInfo(Context context, BoxInfo boxInfo, TamInfo[] tamInfos)
	{
		synchronized(mCachedInfo)
		{
			if ((tamInfos != null) || !boxInfo.getUdn().equals(mCachedInfo.mUdn))
				mCachedInfo.mTamInfos = tamInfos;

			mCachedInfo.mUdn = boxInfo.getUdn();

			if ((mCachedInfo.mTamInfos != null) && (mCachedInfo.mTamInfos.length > 0))
			{
				mCachedInfo.mSelectedTam = null;
				String prefTam = boxInfo.getTam();
				String fmtTamNumber = context.getString(R.string.settings_tam_fmt);
				int visibleTams = 0;
				String fistVisibleTam = null;
				for (TamInfo info : mCachedInfo.mTamInfos)
				{
					if (!info.isDisplay()) continue;
					String tam = String.format(fmtTamNumber,
							info.getIndex() + DataHub.FIRST_TAM_NUMBER_DIGIT);
					if (prefTam.equals(tam))
						mCachedInfo.mSelectedTam = tam;
					visibleTams++;
					if (fistVisibleTam == null) fistVisibleTam = tam; 
				}
				mCachedInfo.mIsInSettings = visibleTams > 1;
				if (!mCachedInfo.mIsInSettings && (mCachedInfo.mSelectedTam == null))
					mCachedInfo.mSelectedTam = (fistVisibleTam == null) ?
							BoxInfo.TAM_DEFAULT : fistVisibleTam;
			}
			else
			{
				mCachedInfo.mSelectedTam = BoxInfo.TAM_DEFAULT;
				mCachedInfo.mIsInSettings = false;
			}
		}									
		onCachedChanged();
	}
	
	@SuppressWarnings("unchecked")
	private static void onCachedChanged()
	{
		ArrayList<WeakReference<TamPreference>> list =
			new ArrayList<WeakReference<TamPreference>>();
		synchronized(mInstances)
		{
			list = (ArrayList<WeakReference<TamPreference>>)mInstances.clone();
		}
		for(WeakReference<TamPreference> item : list)
		{
			TamPreference pref = item.get(); 
			if (pref != null) pref.onChanged();
		}
	}
			
	/**
	 * Checks if preference should be reachable from settings
	 * listener is only called if a result has been fetched (box connected) 
	 *  
	 * @param context
	 * 			a valid context
	 * @param listener
	 * 			callback to get result
	 * @return cached result from last check or fixed result
	 */
	public static boolean isInSettings(Context context,
			final OnResultListener<Boolean> listener)
	{
		boolean instantResult = false;
		
		final BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		if ((listener != null) && (boxInfo != null))
		{
			Tr064Boxinfo tr064Info = boxInfo.getTr064Boxinfo();
			if ((tr064Info != null) && tr064Info.getTr064Capabilities()
					.has(Tr064Capabilities.Capability.TAM))
			{
				getSelectedTam(context, new OnResultListener<String>()
				{
					public void onResult(String udn, String result)
					{
						if (boxInfo.getUdn().equals(udn))
						{
							Boolean isInSettings = null;
							// cached info updated by getSelectedTam 
							synchronized(mCachedInfo)
							{
								if (mCachedInfo.isBox(udn))
									isInSettings = Boolean.valueOf(
											mCachedInfo.mIsInSettings);
							}
							if (isInSettings != null)
							{
								listener.onResult(udn, isInSettings);
								return;
							}
						}
						listener.onError(null);
					}

					public void onError(Exception error)
					{
						listener.onError(error);
					}
				});
				
				synchronized(mCachedInfo)
				{
					if (mCachedInfo.isBox(boxInfo.getUdn()))
						instantResult = mCachedInfo.mIsInSettings;
				}
			}
		}
		
		return instantResult;
	}
	
	/**
	 * Checks if previously selected TAM is valid/fixes setting if needed
	 * listener is only called if a result has been fetched (box connected) 
	 *  
	 * @param context
	 * 			a valid context
	 * @param listener
	 * 			callback to get result
	 * @return cached result from last check or fixed result
	 */
	public static String getSelectedTam(final Context context,
			final OnResultListener<String> listener)
	{
		String instantResult = null;

		final BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		if ((listener != null) && (boxInfo != null))
		{
			Tr064Boxinfo tr064Info = boxInfo.getTr064Boxinfo();
			if ((tr064Info != null) && tr064Info.getTr064Capabilities()
					.has(Tr064Capabilities.Capability.TAM))
			{
				mFritzBox.getTamInfosAsync(context,
						new DataHub.OnCompleteListener<TamInfo[]>()
				{
					public void onComplete(TamInfo[] result, Exception error)
					{
						String selectedTam = null;
						if ((error == null) && (result != null))
						{
							updateCachedInfo(context, boxInfo, result);
							synchronized(mCachedInfo)
							{
								if (mCachedInfo.isBox(boxInfo.getUdn()))
									selectedTam = mCachedInfo.mSelectedTam;
							}
						}
						if (selectedTam != null)
						{
							listener.onResult(boxInfo.getUdn(), selectedTam);
							return;
						}
						listener.onError(error);
					}
				});
				
				synchronized(mCachedInfo)
				{
					if (mCachedInfo.isBox(boxInfo.getUdn()))
						instantResult = mCachedInfo.mSelectedTam;
				}
			}
		}
		
		return instantResult;
	}
	
	/**
	 * Gets cached selected TAM as returned by getSelectedTam()
	 * but without updating TAM info 
	 *  
	 * @return cached result from last check or fixed result
	 */
	public static String getCachedSelectedTam()
	{
		final BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		if (boxInfo != null)
		{
			Tr064Boxinfo tr064Info = boxInfo.getTr064Boxinfo();
			if ((tr064Info != null) && tr064Info.getTr064Capabilities()
					.has(Tr064Capabilities.Capability.TAM))
			{
				synchronized(mCachedInfo)
				{
					if (mCachedInfo.isBox(boxInfo.getUdn()))
						return mCachedInfo.mSelectedTam;
				}
			}
		}
		
		return null;
	}
}
