/* 
 * Copyright 2015 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.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

import de.avm.android.fritzapp.R;
import de.avm.android.fritzapp.sipua.phone.CallerInfo;
import de.avm.android.fritzapp.util.LocalContacts;
import de.avm.android.fritzapp.util.PackageManagerHelper;
import de.avm.android.tr064.model.Call;
import de.avm.fundamentals.logger.FileLog;

import android.app.Activity;
import android.content.AsyncQueryHandler;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.provider.CallLog.Calls;
import android.telephony.PhoneNumberUtils;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.view.ViewTreeObserver;
import android.widget.ListView;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;

/**
 * ListView to show android's call log
 */
public class RecentCallsView extends ListView
{
    private static final String TAG = "RecentCallsView";

    /**
     * data provider query parameters
     */
    static final String[] CALL_LOG_PROJECTION = new String[]
	{
    	Calls._ID,
    	Calls.NUMBER,
    	Calls.DATE,
    	Calls.TYPE,
    	Calls.CACHED_NAME,
    	Calls.CACHED_NUMBER_TYPE,
    	Calls.CACHED_NUMBER_LABEL
    };
    static final int NUMBER_COLUMN_INDEX = 1;
    static final int DATE_COLUMN_INDEX = 2;
    static final int CALL_TYPE_COLUMN_INDEX = 3;
    static final int CALLER_NAME_COLUMN_INDEX = 4;
    static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 5;
    static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 6;

    private static final int QUERY_TOKEN = 53;

    RecentCallsAdapter mAdapter;
    private QueryHandler mQueryHandler;
    private boolean mHasTelephony = true;

    static final class ContactInfo
    {
        public String name = null;
        public int type = -1;
        public String label = null;
        public String number = null;
        public String phoneLabel = null;
        public String formattedNumber = null;
        public static ContactInfo EMPTY = new ContactInfo();
    }

	private class ViewHolder
	{
		public ImageView TypeIcon;
		public TextView Name;
		public TextView NumberLabel;
		public TextView Number;
		public TextView Info;
		
		// call list's original data
		public String CallNumber = null;
		public int CallType = -1;
		public String CallName = null;
		public long CallDate = 0;
		
		public ViewHolder(View view)
		{
			// progress not used here
			view.findViewById(R.id.Progress).setVisibility(View.GONE);
			
			TypeIcon = (ImageView)view.findViewById(R.id.TypeIcon);
			Name = (TextView)view.findViewById(R.id.CallLogEntryName);
			NumberLabel = (TextView)view.findViewById(R.id.CallLogEntryNumberLabel);
			Number = (TextView)view.findViewById(R.id.CallLogEntryNumber);
			Info = (TextView)view.findViewById(R.id.CallLogEntryInfo);
		}
	}

	static final class CallerInfoQuery
    {
        String number;
        int position;
        String name;
        int numberType;
        String numberLabel;
    }
    
    private static final SpannableStringBuilder sEditable = new SpannableStringBuilder();
    private static final int FORMATTING_TYPE_INVALID = -1;
    private static int sFormattingType = FORMATTING_TYPE_INVALID;

    private static final long UPDATE_TIMEOUT = 60000;
	private Timer mUpdateTimeout = null;
    
    public RecentCallsView(Context context)
	{
		super(context);
		init(context);
	}

	public RecentCallsView(Context context, AttributeSet attrs)
	{
		super(context, attrs);
		init(context);
	}
	
	private void init(Context context)
	{
		if (!Activity.class.isAssignableFrom(context.getClass()))
			throw new IllegalArgumentException("Context has to be an activity");

        // don't show call list an a device without telephony
        mHasTelephony = PackageManagerHelper.hasTelephony(context);

        mAdapter = new RecentCallsAdapter(context);
        setAdapter(mAdapter);
        mQueryHandler = new QueryHandler(this);

        // Reset locale-based formatting cache
        sFormattingType = FORMATTING_TYPE_INVALID;
	}
	
	public void onResume()
	{
        // The adapter caches looked up numbers, clear it so they will get
        // looked up again.
        if (mAdapter != null) mAdapter.clearCache();
        startQuery();
        mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
        startUpdateTimeout();
    }

	public void onPause()
	{
		stopUpdateTimeout();
        // Kill the requests thread
        mAdapter.stopRequestProcessing();
	}

    protected void onDestroy()
    {
    	stopUpdateTimeout();
        mAdapter.stopRequestProcessing();
        Cursor cursor = mAdapter.getCursor();
        if (cursor != null && !cursor.isClosed())
            cursor.close();
    }

    private void startUpdateTimeout()
    {
    	stopUpdateTimeout();
		mUpdateTimeout = new Timer();
    	mUpdateTimeout.schedule(new TimerTask()
		{
			public void run()
			{
				mAdapter.redraw();
			}
		}, UPDATE_TIMEOUT, UPDATE_TIMEOUT);
    }
    
    private void stopUpdateTimeout()
    {
		if (mUpdateTimeout != null)
		{
			mUpdateTimeout.cancel();
			mUpdateTimeout.purge();
			mUpdateTimeout = null;
		}
    }
    
    /**
     * Format the given phone number using
     * {@link PhoneNumberUtils#formatNumber(android.text.Editable, int)}. This
     * helper method uses {@link #sEditable} and {@link #sFormattingType} to
     * prevent allocations between multiple calls.
     * <p>
     * Because of the shared {@link #sEditable} builder, <b>this method is not
     * thread safe</b>, and should only be called from the GUI thread.
     */
    private String formatPhoneNumber(Context context, String number)
    {
    	if (TextUtils.isEmpty(number))
    		return context.getString(R.string.unknown);
    	
        // Cache formatting type if not already present
        if (sFormattingType == FORMATTING_TYPE_INVALID)
            sFormattingType = PhoneNumberUtils.getFormatTypeForLocale(Locale.getDefault());
        
        sEditable.clear();
        sEditable.append(number);
        
        PhoneNumberUtils.formatNumber(sEditable, sFormattingType);
        return sEditable.toString();
    }

    private void startQuery()
    {
        // don't show call list an a device without telephony
        if (mHasTelephony)
        {
            mAdapter.setLoading(true);

            // Cancel any pending queries
            mQueryHandler.cancelOperation(QUERY_TOKEN);
            mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI,
                    CALL_LOG_PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER);
        }
    }

    /**
     * Handler to be used by RecentCallsAdapter
     */
    private static class RecentCallsAdapterHandler extends Handler
    {
        public static final int REDRAW = 1;
        public static final int START_THREAD = 2;

        WeakReference<RecentCallsAdapter> mParentRef;
    	
    	public RecentCallsAdapterHandler(RecentCallsAdapter parent)
    	{
    		super();
    		mParentRef = new WeakReference<RecentCallsAdapter>(parent);
    	}
    	
        public void handleMessage(Message msg)
        {
        	RecentCallsAdapter parent = mParentRef.get();
    		if (parent != null)
    		{
	            switch (msg.what)
	            {
	                case REDRAW:
	                	parent.notifyDataSetChanged();
	                    break;
	                case START_THREAD:
	                	parent.startRequestProcessing();
	                    break;
	            }
    		}
        }
    }

    /**
     * List view adapter
     */
    final class RecentCallsAdapter extends ResourceCursorAdapter
    		implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener
    {
        final HashMap<String,ContactInfo> mContactInfo = new HashMap<>();
        private final LinkedList<CallerInfoQuery> mRequests = new LinkedList<>();
        private volatile boolean mDone;
        private boolean mLoading = true;
        ViewTreeObserver.OnPreDrawListener mPreDrawListener = null;
        private boolean mFirst;
        private Thread mCallerIdThread;

        private Drawable mDrawableIncoming;
        private Drawable mDrawableOutgoing;
        private Drawable mDrawableMissed;

        private RecentCallsAdapterHandler mHandler =
        		new RecentCallsAdapterHandler(this);

        public void setLoading(boolean loading)
        {
            mLoading = loading;
        }
        
        public void redraw()
        {
            synchronized (mRequests)
            {
            	// redraw if no requests pending
                if (mRequests.isEmpty())
                    mHandler.sendEmptyMessage(RecentCallsAdapterHandler.REDRAW);
            }
        }
        
        
        public RecentCallsAdapter(Context context)
        {
            super(context, R.layout.t_callloglistitem, null);

            mDrawableIncoming = context.getResources()
            		.getDrawable(R.drawable.call_incoming);
            mDrawableOutgoing = context.getResources()
            		.getDrawable(R.drawable.call_outgoing);
            mDrawableMissed = context.getResources()
            		.getDrawable(R.drawable.call_missed);
        }

        public void onClick(View view)
        {
        	ViewHolder viewHolder = (ViewHolder)view.getTag();
            String number = viewHolder.CallNumber;
            int type = viewHolder.CallType;
            
            // collect call's data
            ContactInfo info = mContactInfo.get(number);
            if (info == ContactInfo.EMPTY) info = null;

            Call call = new Call();
        	switch(type)
        	{
                case Calls.OUTGOING_TYPE:
        			call.setType(Call.CALL_TYPE.OUTGOING);
        			call.setCalledNumber(number);
        			break;

                case Calls.INCOMING_TYPE:
        			call.setType(Call.CALL_TYPE.INCOMING);
        			call.setCallerNumber(number);
        			break;

                case Calls.MISSED_TYPE:
        			call.setType(Call.CALL_TYPE.MISSED);
        			call.setCallerNumber(number);
        			break;
        			
                default:
        			call.setType(Call.CALL_TYPE.UNSPECIFIED);
        			break;
        	}
        	String partnerName = (info == null) ? null : info.name;
        	if (TextUtils.isEmpty(partnerName)) partnerName = viewHolder.CallName; 
        	call.setPartnerName(partnerName);
        	call.setTimeStamp(new Date(viewHolder.CallDate));

            // open details activity
        	Context context = RecentCallsView.this.getContext();
    		Intent intent = new Intent(context, CallDetailsActivity.class);
    		intent.putExtra(CallDetailsActivity.EXTRA_CALL_DATA, call);
    		intent.putExtra(CallDetailsActivity.EXTRA_CALL_IGNORE_PORTS, true);
    		context.startActivity(intent);
        }

        public boolean onPreDraw()
        {
            if (mFirst)
            {
                mHandler.sendEmptyMessageDelayed(
                		RecentCallsAdapterHandler.START_THREAD, 1000);
                mFirst = false;
            }
            return true;
        }

        /**
         * Requery on background thread when {@link Cursor} changes.
         */
        @Override
        protected void onContentChanged()
        {
            startQuery(); // start async requery
        }

        @Override
        public boolean isEmpty()
        {
        	// don't show empty state when loading
            return !mLoading && super.isEmpty();
        }

        public ContactInfo getContactInfo(String number)
        {
            return mContactInfo.get(number);
        }

        public void startRequestProcessing()
        {
            mDone = false;
            mCallerIdThread = new Thread(this);
            mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
            mCallerIdThread.start();
        }

        public void stopRequestProcessing()
        {
            mDone = true;
            if (mCallerIdThread != null) mCallerIdThread.interrupt();
        }

        public void clearCache()
        {
            synchronized (mContactInfo)
            {
                mContactInfo.clear();
            }
        }

        private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci)
        {
            // Check if they are different. If not, don't update.
            if (TextUtils.equals(ciq.name, ci.name)
                    && TextUtils.equals(ciq.numberLabel, ci.label)
                    && ciq.numberType == ci.type)
                return;

            ContentValues values = new ContentValues(3);
            values.put(Calls.CACHED_NAME, ci.name);
            values.put(Calls.CACHED_NUMBER_TYPE, ci.type);
            values.put(Calls.CACHED_NUMBER_LABEL, ci.label);
            try
            {
	            RecentCallsView.this.getContext().getContentResolver()
	            		.update(Calls.CONTENT_URI, values,
	            				Calls.NUMBER + "='" + ciq.number + "'", null);
            }
            catch(Exception e)
            {
                FileLog.w(TAG, e.getMessage(), e);
            }
        }

        private void enqueueRequest(String number, int position, String name,
        		int numberType, String numberLabel)
        {
            CallerInfoQuery ciq = new CallerInfoQuery();
            ciq.number = number;
            ciq.position = position;
            ciq.name = name;
            ciq.numberType = numberType;
            ciq.numberLabel = numberLabel;
            synchronized (mRequests)
            {
                mRequests.add(ciq);
                mRequests.notifyAll();
            }
        }

        private void queryContactInfo(CallerInfoQuery ciq)
        {
        	// First check if there was a prior request for the same number
            // that was already satisfied
            ContactInfo info = mContactInfo.get(ciq.number);
            if (info == null || (info == ContactInfo.EMPTY))
            {
                Cursor phonesCursor = RecentCallsView.this.getContext()
                		.getContentResolver().query(LocalContacts.getInstance()
                				.getCallerInfoUri(ciq.number),
                				LocalContacts.getInstance().getCallerInfoProjection(),
                				LocalContacts.getInstance().getSelection(), null, null);
                
                if (phonesCursor != null)
                {
                    if (phonesCursor.moveToFirst())
                    {
                    	CallerInfo callerInfo = LocalContacts.getInstance()
                    			.getCallerInfoItem(RecentCallsView.this
                    					.getContext(), phonesCursor);
                    	info = new ContactInfo();
                    	info.name = callerInfo.name;
                    	info.number = callerInfo.phoneNumber;
                    	info.type = callerInfo.numberType;
                    	info.label = callerInfo.numberLabel;
                    	info.phoneLabel = callerInfo.phoneLabel;

                        // New incoming phone number invalidates our formatted
                        // cache. Any cache fills happen only on the GUI thread.
                        info.formattedNumber = null;

                        mContactInfo.put(ciq.number, info);
                        // Inform list to update this item, if in view
                        redraw();
                    }
                    phonesCursor.close();
                }
            }
            else redraw();

            if (info != null) updateCallLog(ciq, info);
        }

        /*
         * Handles requests for contact name and number type
         * @see java.lang.Runnable#run()
         */
        public void run()
        {
            while (!mDone)
            {
                CallerInfoQuery ciq = null;
                synchronized (mRequests)
                {
                    if (mRequests.isEmpty())
                    {
                        try
                        {
                            mRequests.wait(1000);
                        }
                        catch (InterruptedException ie)
                        {
                            // Ignore and continue processing requests
                        }
                    }
                    else ciq = mRequests.removeFirst();
                }
                if (ciq != null)
                {
                	try
                	{
                		queryContactInfo(ciq);
                	}
                	catch(Exception e)
                	{
                        FileLog.w(TAG, e.getMessage(), e);
                	}
                }
            }
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent)
        {
            View view = super.newView(context, cursor, parent);
            view.setOnClickListener(this);

            // Get the views to bind to
            view.setTag(new ViewHolder(view));

            return view;
        }

        @Override
        public void bindView(View view, Context context, Cursor c)
        {
            final ViewHolder views = (ViewHolder)view.getTag();

            try
            {
            	views.CallNumber = c.getString(NUMBER_COLUMN_INDEX);
                if (views.CallNumber.startsWith("-"))
                	views.CallNumber = ""; // unknown caller
            }
            catch(Exception e)
            {
            	views.CallNumber = "";
            }
            String formattedNumber = null;
            views.CallName = c.getString(CALLER_NAME_COLUMN_INDEX);
            int callerNumberType = c.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
            String callerNumberLabel = c.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
            String numberLabel = null;
            views.CallType = c.getInt(CALL_TYPE_COLUMN_INDEX);
            views.CallDate = c.getLong(DATE_COLUMN_INDEX);

            // Lookup contacts with this number
            ContactInfo info = (TextUtils.isEmpty(views.CallNumber)) ?
            		null : mContactInfo.get(views.CallNumber);
            if (info == null)
            {
                // Mark it as empty and queue up a request to find the name
                // The db request should happen on a non-UI thread
                info = ContactInfo.EMPTY;
                mContactInfo.put(views.CallNumber, info);
                if (!TextUtils.isEmpty(views.CallNumber))
	                enqueueRequest(views.CallNumber, c.getPosition(), views.CallName,
	                		callerNumberType, callerNumberLabel);
            }
            else if (info != ContactInfo.EMPTY)
            { 	// Has been queried
                // Check if any data is different from the data cached in the
                // calls db. If so, queue the request so that we can update
                // the calls db.
                if (!TextUtils.equals(info.name, views.CallName)
                        || info.type != callerNumberType
                        || !TextUtils.equals(info.label, callerNumberLabel))
                    // Something is amiss, so sync up.
                    enqueueRequest(views.CallNumber, c.getPosition(),
                    		views.CallName, callerNumberType, callerNumberLabel);
                
                // Format and cache phone number for found contact
                if (info.formattedNumber == null)
                    info.formattedNumber = formatPhoneNumber(context, info.number);
                formattedNumber = info.formattedNumber;
                numberLabel = info.phoneLabel;
            }

            String name = info.name;
            // If there's no name cached in our hashmap, but there's one in the
            // calls db, use the one in the calls db. Otherwise the name in our
            // hashmap is more recent, so it has precedence.
            if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(views.CallName))
            {
                name = views.CallName;
                numberLabel = LocalContacts.getInstance().getDisplayPhoneLabel(
                		context, callerNumberType, callerNumberLabel).toString();
                
                // Format the cached call_log phone number
                formattedNumber = formatPhoneNumber(context, views.CallNumber);
            }
            // Set the text lines
            if (!TextUtils.isEmpty(name))
            {
                views.Name.setText(name);
                views.Number.setText(formattedNumber);
                if (!TextUtils.isEmpty(numberLabel))
                {
                    views.NumberLabel.setText(numberLabel);
                    views.NumberLabel.setVisibility(View.VISIBLE);
                }
                else views.NumberLabel.setVisibility(View.GONE);
            }
            else
            {
                views.Name.setText(formatPhoneNumber(context, views.CallNumber));
                views.Number.setText("");
                views.NumberLabel.setVisibility(View.GONE);
            }

            // Set the date/time field by mixing relative and absolute times.
            views.Info.setText(DateUtils.getRelativeTimeSpanString(views.CallDate,
                    System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
                    DateUtils.FORMAT_ABBREV_RELATIVE));

            // Set the icon
            switch (views.CallType)
            {
                case Calls.INCOMING_TYPE:
                    views.TypeIcon.setImageDrawable(mDrawableIncoming);
                    break;

                case Calls.OUTGOING_TYPE:
                    views.TypeIcon.setImageDrawable(mDrawableOutgoing);
                    break;

                case Calls.MISSED_TYPE:
                    views.TypeIcon.setImageDrawable(mDrawableMissed);
                    break;
            }

            // Listen for the first draw
            if (mPreDrawListener == null)
            {
                mFirst = true;
                mPreDrawListener = this;
                view.getViewTreeObserver().addOnPreDrawListener(this);
            }
        }
    }

    private static final class QueryHandler extends AsyncQueryHandler
    {
        private final WeakReference<RecentCallsView> mListView;

        public QueryHandler(RecentCallsView view)
        {
            super(view.getContext().getContentResolver());
            mListView = new WeakReference<>(view);
        }

        @Override
        protected void onQueryComplete(int token, Object cookie, Cursor cursor)
        {
        	final RecentCallsView view = mListView.get();
            if ((view != null) &&
            	Activity.class.isAssignableFrom(view.getContext().getClass()) &&
            	!((Activity)view.getContext()).isFinishing())
            {
                final RecentCallsView.RecentCallsAdapter callsAdapter = view.mAdapter;
                callsAdapter.setLoading(false);
                callsAdapter.changeCursor(cursor);
            }
            else cursor.close();
        }
    }
}
