/* ownCloud Android client application
 *   Copyright (C) 2012-2013 ownCloud Inc.
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License version 2,
 *   as published by the Free Software Foundation.
 *
 *   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.owncloud.android.oc_framework.operations;

import java.io.IOException;

import org.apache.commons.httpclient.Credentials;

import com.owncloud.android.oc_framework.network.BearerCredentials;
import com.owncloud.android.oc_framework.network.webdav.WebdavClient;
import com.owncloud.android.oc_framework.network.webdav.OwnCloudClientFactory;
import com.owncloud.android.oc_framework.operations.RemoteOperationResult.ResultCode;



import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountsException;
import android.app.Activity;
import android.content.Context;
import android.os.Handler;
import android.util.Log;


/**
 * Operation which execution involves one or several interactions with an ownCloud server.
 * 
 * Provides methods to execute the operation both synchronously or asynchronously.
 * 
 * @author David A. Velasco 
 */
public abstract class RemoteOperation implements Runnable {
	
    private static final String TAG = RemoteOperation.class.getSimpleName();

    /** ownCloud account in the remote ownCloud server to operate */
    private Account mAccount = null;
    
    /** Android Application context */
    private Context mContext = null;
    
	/** Object to interact with the remote server */
	private WebdavClient mClient = null;
	
	/** Callback object to notify about the execution of the remote operation */
	private OnRemoteOperationListener mListener = null;
	
	/** Handler to the thread where mListener methods will be called */
	private Handler mListenerHandler = null;

	/** Activity */
    private Activity mCallerActivity;

	
	/**
	 *  Abstract method to implement the operation in derived classes.
	 */
	protected abstract RemoteOperationResult run(WebdavClient client); 
	

    /**
     * Synchronously executes the remote operation on the received ownCloud account.
     * 
     * Do not call this method from the main thread.
     * 
     * This method should be used whenever an ownCloud account is available, instead of {@link #execute(WebdavClient)}. 
     * 
     * @param account   ownCloud account in remote ownCloud server to reach during the execution of the operation.
     * @param context   Android context for the component calling the method.
     * @return          Result of the operation.
     */
    public final RemoteOperationResult execute(Account account, Context context) {
        if (account == null)
            throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account");
        if (context == null)
            throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context");
        mAccount = account;
        mContext = context.getApplicationContext();
        try {
            mClient = OwnCloudClientFactory.createOwnCloudClient(mAccount, mContext);
        } catch (Exception e) {
            Log.e(TAG, "Error while trying to access to " + mAccount.name, e);
            return new RemoteOperationResult(e);
        }
        return run(mClient);
    }
    
	
	/**
	 * Synchronously executes the remote operation
	 * 
     * Do not call this method from the main thread.
     * 
	 * @param client	Client object to reach an ownCloud server during the execution of the operation.
	 * @return			Result of the operation.
	 */
	public final RemoteOperationResult execute(WebdavClient client) {
		if (client == null)
			throw new IllegalArgumentException("Trying to execute a remote operation with a NULL WebdavClient");
		mClient = client;
		return run(client);
	}

	
    /**
     * Asynchronously executes the remote operation
     * 
     * This method should be used whenever an ownCloud account is available, instead of {@link #execute(WebdavClient)}. 
     * 
     * @param account           ownCloud account in remote ownCloud server to reach during the execution of the operation.
     * @param context           Android context for the component calling the method.
     * @param listener          Listener to be notified about the execution of the operation.
     * @param listenerHandler   Handler associated to the thread where the methods of the listener objects must be called.
     * @return                  Thread were the remote operation is executed.
     */
    public final Thread execute(Account account, Context context, OnRemoteOperationListener listener, Handler listenerHandler, Activity callerActivity) {
        if (account == null)
            throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account");
        if (context == null)
            throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context");
        mAccount = account;
        mContext = context.getApplicationContext();
        mCallerActivity = callerActivity;
        mClient = null;     // the client instance will be created from mAccount and mContext in the runnerThread to create below
        
        mListener = listener;
        
        mListenerHandler = listenerHandler;
        
        Thread runnerThread = new Thread(this);
        runnerThread.start();
        return runnerThread;
    }

    
	/**
	 * Asynchronously executes the remote operation
	 * 
	 * @param client			Client object to reach an ownCloud server during the execution of the operation.
	 * @param listener			Listener to be notified about the execution of the operation.
	 * @param listenerHandler	Handler associated to the thread where the methods of the listener objects must be called.
	 * @return					Thread were the remote operation is executed.
	 */
	public final Thread execute(WebdavClient client, OnRemoteOperationListener listener, Handler listenerHandler) {
		if (client == null) {
			throw new IllegalArgumentException("Trying to execute a remote operation with a NULL WebdavClient");
		}
		mClient = client;
		
		if (listener == null) {
			throw new IllegalArgumentException("Trying to execute a remote operation asynchronously without a listener to notiy the result");
		}
		mListener = listener;
		
		if (listenerHandler == null) {
			throw new IllegalArgumentException("Trying to execute a remote operation asynchronously without a handler to the listener's thread");
		}
		mListenerHandler = listenerHandler;
		
		Thread runnerThread = new Thread(this);
		runnerThread.start();
		return runnerThread;
	}
	
    /**
     * Synchronously retries the remote operation using the same WebdavClient in the last call to {@link RemoteOperation#execute(WebdavClient)}
     * 
     * @param listener          Listener to be notified about the execution of the operation.
     * @param listenerHandler   Handler associated to the thread where the methods of the listener objects must be called.
     * @return                  Thread were the remote operation is executed.
     */
    public final RemoteOperationResult retry() {
        return execute(mClient);
    }
    
    /**
     * Asynchronously retries the remote operation using the same WebdavClient in the last call to {@link RemoteOperation#execute(WebdavClient, OnRemoteOperationListener, Handler)}
     * 
     * @param listener          Listener to be notified about the execution of the operation.
     * @param listenerHandler   Handler associated to the thread where the methods of the listener objects must be called.
     * @return                  Thread were the remote operation is executed.
     */
    public final Thread retry(OnRemoteOperationListener listener, Handler listenerHandler) {
        return execute(mClient, listener, listenerHandler);
    }
	
	
	/**
	 * Asynchronous execution of the operation 
	 * started by {@link RemoteOperation#execute(WebdavClient, OnRemoteOperationListener, Handler)}, 
	 * and result posting.
	 * 
	 * TODO refactor && clean the code; now it's a mess
	 */
    @Override
    public final void run() {
        RemoteOperationResult result = null;
        boolean repeat = false;
        do {
            try{
                if (mClient == null) {
                    if (mAccount != null && mContext != null) {
                        if (mCallerActivity != null) {
                            mClient = OwnCloudClientFactory.createOwnCloudClient(mAccount, mContext, mCallerActivity);
                        } else {
                            mClient = OwnCloudClientFactory.createOwnCloudClient(mAccount, mContext);
                        }
                    } else {
                        throw new IllegalStateException("Trying to run a remote operation asynchronously with no client instance or account");
                    }
                }
            
            } catch (IOException e) {
                Log.e(TAG, "Error while trying to access to " + mAccount.name, new AccountsException("I/O exception while trying to authorize the account", e));
                result = new RemoteOperationResult(e);
            
            } catch (AccountsException e) {
                Log.e(TAG, "Error while trying to access to " + mAccount.name, e);
                result = new RemoteOperationResult(e);
            }
    	
            if (result == null)
                result = run(mClient);
        
            repeat = false;
            if (mCallerActivity != null && mAccount != null && mContext != null && !result.isSuccess() &&
//                    (result.getCode() == ResultCode.UNAUTHORIZED || (result.isTemporalRedirection() && result.isIdPRedirection()))) {
                    (result.getCode() == ResultCode.UNAUTHORIZED || result.isIdPRedirection())) {
                /// possible fail due to lack of authorization in an operation performed in foreground
                Credentials cred = mClient.getCredentials();
                String ssoSessionCookie = mClient.getSsoSessionCookie();
                if (cred != null || ssoSessionCookie != null) {
                    /// confirmed : unauthorized operation
                    AccountManager am = AccountManager.get(mContext);
                    boolean bearerAuthorization = (cred != null && cred instanceof BearerCredentials);
                    boolean samlBasedSsoAuthorization = (cred == null && ssoSessionCookie != null);
                    if (bearerAuthorization) {
                        am.invalidateAuthToken(mAccount.type, ((BearerCredentials)cred).getAccessToken());
                    } else if (samlBasedSsoAuthorization ) {
                        am.invalidateAuthToken(mAccount.type, ssoSessionCookie);
                    } else {
                        am.clearPassword(mAccount);
                    }
                    mClient = null;
                    repeat = true;  // when repeated, the creation of a new OwnCloudClient after erasing the saved credentials will trigger the login activity
                    result = null;
                }
            }
        } while (repeat);
        
        final RemoteOperationResult resultToSend = result;
        if (mListenerHandler != null && mListener != null) {
        	mListenerHandler.post(new Runnable() {
                @Override
                public void run() {
                    mListener.onRemoteOperationFinish(RemoteOperation.this, resultToSend);
                }
            });
        }
    }


    /**
     * Returns the current client instance to access the remote server.
     * 
     * @return      Current client instance to access the remote server.
     */
    public final WebdavClient getClient() {
        return mClient;
    }


}
