/**
 * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
 * This file is part of CSipSimple.
 *
 *  CSipSimple is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  If you own a pjsip commercial license you can also redistribute it
 *  and/or modify it under the terms of the GNU Lesser General Public License
 *  as an android library.
 *
 *  CSipSimple 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 for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with CSipSimple.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.voiceblue.phone.ui.account;

import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.provider.BaseColumns;
import android.support.v4.content.AsyncTaskLoader;
import android.text.TextUtils;

import com.voiceblue.phone.api.SipProfile;
import com.voiceblue.phone.models.Filter;
import com.voiceblue.phone.utils.AccountListUtils;
import com.voiceblue.phone.utils.AccountListUtils.AccountStatusDisplay;
import com.voiceblue.phone.utils.CallHandlerPlugin;
import com.voiceblue.phone.utils.CallHandlerPlugin.OnLoadListener;
import com.voiceblue.phone.utils.Log;
import com.voiceblue.phone.utils.PreferencesProviderWrapper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class AccountsLoader extends AsyncTaskLoader<Cursor> {

    public static final String FIELD_FORCE_CALL = "force_call";
    public static final String FIELD_NBR_TO_CALL = "nbr_to_call";
    public static final String FIELD_STATUS_OUTGOING = "status_for_outgoing";
    public static final String FIELD_STATUS_COLOR = "status_color";
    

    private static final String THIS_FILE = "OutgoingAccountsLoader";

    private Cursor currentResult;

    private final String numberToCall;
    private final boolean ignoreRewritting;
    private final boolean loadStatus;
    private final boolean onlyActive;
    private final boolean loadCallHandlerPlugins;
    
    
    /**
     * Constructor for loader for outgoing call context. <br/>
     * This one will care of rewriting number and keep track of accounts status.
     * @param context Your app context
     * @param number Phone number for outgoing call
     * @param ignoreRewrittingRules Should we ignore rewriting rules.
     */
    public AccountsLoader(Context context, String number, boolean ignoreRewrittingRules) {
        super(context);
        numberToCall = number;
        ignoreRewritting = ignoreRewrittingRules;
        loadStatus = true;
        onlyActive = true;
        loadCallHandlerPlugins = true;
        initHandlers();
    }
    

    public AccountsLoader(Context context, boolean onlyActiveAccounts, boolean withCallHandlerPlugins) {
        super(context);
        numberToCall = "";
        ignoreRewritting = true;
        loadStatus = false;
        onlyActive = onlyActiveAccounts;
        loadCallHandlerPlugins = withCallHandlerPlugins;
        initHandlers();
    }
    
    private void initHandlers() {
        CallHandlerPlugin.initHandler();
        loaderObserver = new ForceLoadContentObserver();
    }

    private ContentObserver loaderObserver;
    private ArrayList<FilteredProfile> finalAccounts;
    
    
    @Override
    public Cursor loadInBackground() {
        // First register for status updates
        if(loadStatus) {
            getContext().getContentResolver().registerContentObserver(SipProfile.ACCOUNT_STATUS_URI,
                true, loaderObserver);
        }
        ArrayList<FilteredProfile> prefinalAccounts = new ArrayList<FilteredProfile>();
        
        // Get all sip profiles
        ArrayList<SipProfile> accounts;
        PreferencesProviderWrapper prefsWrapper = new PreferencesProviderWrapper(getContext());
        if(onlyActive && !prefsWrapper.isValidConnectionForOutgoing() ) {
            accounts = new ArrayList<SipProfile>();
        }else {
            accounts = SipProfile.getAllProfiles(getContext(), onlyActive,
                    new String[] {
                            SipProfile.FIELD_ID,
                            SipProfile.FIELD_ACC_ID,
                            SipProfile.FIELD_ACTIVE,
                            SipProfile.FIELD_DISPLAY_NAME,
                            SipProfile.FIELD_WIZARD
                    });
        }
        // And all external call handlers
        Map<String, String> externalHandlers;
        if(loadCallHandlerPlugins) {
            externalHandlers = CallHandlerPlugin.getAvailableCallHandlers(getContext());
        }else {
            externalHandlers = new HashMap<String, String>();
        }
        if(TextUtils.isEmpty(numberToCall)) {
            // In case of empty number to call, just add everything without any other question
            for(SipProfile acc : accounts) {
                prefinalAccounts.add(new FilteredProfile(acc, false));
            }
            for(Entry<String, String> extEnt : externalHandlers.entrySet() ) {
                prefinalAccounts.add(new FilteredProfile(extEnt.getKey(), false));
                
            }
        }else {
            // If there is a number to call, add only those callable, and flag must call entries
            // Note that we keep processing all call handlers voluntarily cause we may encounter a sip account that doesn't register
            // But is in force call mode
            for(SipProfile acc : accounts) {
                if(Filter.isCallableNumber(getContext(), acc.id, numberToCall)) {
                    boolean forceCall = Filter.isMustCallNumber(getContext(), acc.id, numberToCall);
                    prefinalAccounts.add(new FilteredProfile(acc, forceCall));
                }
            }
            for(Entry<String, String> extEnt : externalHandlers.entrySet() ) {
                long accId = CallHandlerPlugin.getAccountIdForCallHandler(getContext(), extEnt.getKey());
                if(Filter.isCallableNumber(getContext(), accId, numberToCall)) {
                    boolean forceCall = Filter.isMustCallNumber(getContext(), accId, numberToCall);
                    prefinalAccounts.add(new FilteredProfile(extEnt.getKey(), forceCall));
                    if(forceCall) {
                        break;
                    }
                }
            }
            
        }
        
        
        // Build final cursor based on final filtered accounts
        Cursor[] cursorsToMerge = new Cursor[prefinalAccounts.size()];
        int i = 0;
        for (FilteredProfile acc : prefinalAccounts) {
            cursorsToMerge[i++] = createCursorForAccount(acc);
        }

        
        if(cursorsToMerge.length > 0) {
            MergeCursor mg = new MergeCursor(cursorsToMerge);
            mg.registerContentObserver(loaderObserver);
            finalAccounts = prefinalAccounts;
            return mg;
        }else {
            finalAccounts = prefinalAccounts;
            return null;
        }
    }
    

    /**
     * Class to hold information about a possible call handler entry.
     * This could be either a sip profile or a call handler plugin
     */
    private class FilteredProfile {
        /**
         * Sip profile constructor.
         * To use when input is a sip profile
         * @param acc The corresponding sip profile
         * @param forceCall The force call flag in current context.
         */
        public FilteredProfile(SipProfile acc, boolean forceCall) {
            account = acc;
            isForceCall = forceCall;
            AccountStatusDisplay displayState = AccountListUtils.getAccountDisplay(getContext(), acc.id);
            statusColor = displayState.statusColor;
            statusForOutgoing = displayState.availableForCalls;
            callHandlerPlugin = null;
        }
        
        /**
         * Call handler plugin constructor.
         * To use when input is a call handler plugin.
         * @param componentName The component name of the plugin
         * @param forceCall The force call flag in current context.
         */
        public FilteredProfile(String componentName, boolean forceCall) {
            account = new SipProfile();
            long accId = CallHandlerPlugin.getAccountIdForCallHandler(getContext(), componentName);
            account.id = accId;
            account.wizard = "EXPERT";
            CallHandlerPlugin ch = new CallHandlerPlugin(getContext());
            final Semaphore semaphore = new Semaphore(0);
            
            String toCall = numberToCall;
            if(!ignoreRewritting) {
                toCall = Filter.rewritePhoneNumber(getContext(), accId, numberToCall);
            }
            ch.loadFrom(accId, toCall, new OnLoadListener() {
                @Override
                public void onLoad(CallHandlerPlugin ch) {
                    Log.d(THIS_FILE, "Callhandler loaded");
                    semaphore.release();
                }
            });
            boolean succeedInLoading = false;
            try {
                succeedInLoading = semaphore.tryAcquire(3L, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.e(THIS_FILE, "Not possible to bind callhandler plugin");
            }
            if(!succeedInLoading) {
                Log.e(THIS_FILE, "Unreachable callhandler plugin " + componentName);
            }
            account.display_name = ch.getLabel();
            account.icon = ch.getIcon();
            
            isForceCall = forceCall;
            statusColor = getContext().getResources().getColor(android.R.color.white);
            statusForOutgoing = true;
            callHandlerPlugin = ch;
        }
        
        final SipProfile account;
        final boolean isForceCall;
        final private boolean statusForOutgoing;
        final private int statusColor;
        final CallHandlerPlugin callHandlerPlugin;
        
        /**
         * Rewrite a number for this calling entry
         * @param number The number to rewrite
         * @return Rewritten number.
         */
        public String rewriteNumber(String number) {
            if(ignoreRewritting) {
                return number;
            }else {
                return Filter.rewritePhoneNumber(getContext(), account.id, number);
            }
        }
        
        /**
         * Is the account available for outgoing calls
         * @return True if a call can be made using this calling entry
         */
        public boolean getStatusForOutgoing() {
            return statusForOutgoing;
        }
        
        /**
         * The color representing the calling entry status. green for registered
         * sip accounts, red for invalid sip accounts, orange for sip accounts
         * with ongoing registration, white for call handler plugins
         * 
         * @return the color for this entry status.
         */
        public int getStatusColor() {
            return statusColor;
        }
        
        /**
         * Get the eventual associated call handler plugin object.
         * 
         * @return The call handler plugin object if any associated to this
         *         calling entry. Null if representing a sip account.
         */
        public CallHandlerPlugin getCallHandlerPlugin() {
            return callHandlerPlugin;
        }
        
    }

    /**
     * Called when there is new data to deliver to the client. The super class
     * will take care of delivering it; the implementation here just adds a
     * little more logic.
     */
    @Override
    public void deliverResult(Cursor c) {
        if (isReset()) {
            // An async query came in while the loader is stopped. We
            // don't need the result.
            if (currentResult != null) {
                onReleaseResources(currentResult);
            }
        }
        
        currentResult = c;

        if (isStarted()) {
            // If the Loader is currently started, we can immediately
            // deliver its results.
            super.deliverResult(c);
        }
    }

    /**
     * Handles a request to start the Loader.
     */
    @Override
    protected void onStartLoading() {
        if (currentResult != null && !takeContentChanged()) {
            // If we currently have a result available, deliver it
            // immediately.
            deliverResult(currentResult);
        }else {
            forceLoad();
        }
    }

    /**
     * Handles a request to stop the Loader.
     */
    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    /**
     * Handles a request to cancel a load.
     */
    @Override
    public void onCanceled(Cursor c) {
        super.onCanceled(c);

        // At this point we can release the resources associated with 'apps'
        // if needed.
        onReleaseResources(c);
        
    }

    /**
     * Handles a request to completely reset the Loader.
     */
    @Override
    protected void onReset() {
        super.onReset();

        // Ensure the loader is stopped
        onStopLoading();

        // At this point we can release the resources associated with 'apps'
        // if needed.
        if (currentResult != null) {
            onReleaseResources(currentResult);
            currentResult = null;
        }
    }

    /**
     * Helper function to take care of releasing resources associated with an
     * actively loaded data set.
     */
    protected void onReleaseResources(Cursor c) {
        if(c != null) {
            c.unregisterContentObserver(loaderObserver);
            c.close();
        }
        if(loadStatus) {
            getContext().getContentResolver().unregisterContentObserver(loaderObserver);
        }
    }

    
    private static String[] COLUMN_HEADERS = new String[] {
            BaseColumns._ID,
            SipProfile.FIELD_ID,
            SipProfile.FIELD_DISPLAY_NAME,
            SipProfile.FIELD_WIZARD,
            FIELD_FORCE_CALL,
            FIELD_NBR_TO_CALL,
            FIELD_STATUS_OUTGOING,
            FIELD_STATUS_COLOR
    };

    /**
     * Creates a cursor that contains a single row and maps the section to the
     * given value.
     */
    private Cursor createCursorForAccount(FilteredProfile fa) {
        MatrixCursor matrixCursor = new MatrixCursor(COLUMN_HEADERS);
        
        
        matrixCursor.addRow(new Object[] {
                fa.account.id,
                fa.account.id,
                fa.account.display_name,
                fa.account.wizard,
                fa.isForceCall ? 1 : 0,
                fa.rewriteNumber(numberToCall),
                fa.getStatusForOutgoing() ? 1 : 0,
                fa.getStatusColor()
        });
        return matrixCursor;
    }
    
    /**
     * Get the cached call handler plugin loaded for a given position
     * @param position The position to search at
     * @return The call handler plugin if any for this position
     */
    public CallHandlerPlugin getCallHandlerWithAccountId(long accId) {
        for(FilteredProfile filteredAcc :finalAccounts) {
            if(filteredAcc.account.id == accId)
            return filteredAcc.getCallHandlerPlugin();
        }
        return null;
    }


}
