/*
 Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FLOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program 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; version 2
  of the License.

  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, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  USA
 
 */

package com.mysql.jdbc;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public class FailoverConnectionProxy extends LoadBalancingConnectionProxy {
	class FailoverInvocationHandler extends
			ConnectionErrorFiringInvocationHandler {

		public FailoverInvocationHandler(Object toInvokeOn) {
			super(toInvokeOn);
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			String methodName = method.getName();

			if (failedOver && methodName.indexOf("execute") != -1) {
				queriesIssuedFailedOver++;
			}

			return super.invoke(proxy, method, args);
		}

	}

	boolean failedOver;
	boolean hasTriedMaster;
	private long masterFailTimeMillis;
	boolean preferSlaveDuringFailover;
	private String primaryHostPortSpec;
	private long queriesBeforeRetryMaster;
	long queriesIssuedFailedOver;
	private int secondsBeforeRetryMaster;
	
	FailoverConnectionProxy(List<String> hosts, Properties props) throws SQLException {
		super(hosts, props);
		ConnectionPropertiesImpl connectionProps = new ConnectionPropertiesImpl();
		connectionProps.initializeProperties(props);
		
		this.queriesBeforeRetryMaster = connectionProps.getQueriesBeforeRetryMaster();
		this.secondsBeforeRetryMaster = connectionProps.getSecondsBeforeRetryMaster();
		this.preferSlaveDuringFailover = false;
	}
	
	protected ConnectionErrorFiringInvocationHandler createConnectionProxy(
			Object toProxy) {
		return new FailoverInvocationHandler(toProxy);
	}

	// slightly different behavior than load balancing, we only pick a new
	// connection if we've issued enough queries or enough time has passed
	// since we failed over, and that's all handled in pickNewConnection().
	synchronized void dealWithInvocationException(InvocationTargetException e)
			throws SQLException, Throwable, InvocationTargetException {
		Throwable t = e.getTargetException();

		if (t != null) {
			if (failedOver) { // try and fall back
				createPrimaryConnection();
				
				if (this.currentConn != null) {
					throw t;
				}
			}
			
			failOver();
			
			throw t;
		}

		throw e;
	}

	public synchronized Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		String methodName = method.getName();
		
		if ("setPreferSlaveDuringFailover".equals(methodName)) {
			preferSlaveDuringFailover = ((Boolean) args[0]).booleanValue();
		} else if ("clearHasTriedMaster".equals(methodName)) {
			hasTriedMaster = false;
		} else if ("hasTriedMaster".equals(methodName)) {
			return Boolean.valueOf(hasTriedMaster);
		} else if ("isMasterConnection".equals(methodName)) {
			return Boolean.valueOf(!failedOver);
		} else if ("isSlaveConnection".equals(methodName)) {
			return Boolean.valueOf(failedOver);
		} else if ("setReadOnly".equals(methodName)) {
			if (failedOver) {
				return null; // no-op when failed over
			}
		} else if ("setAutoCommit".equals(methodName) && failedOver && 
				shouldFallBack() && Boolean.TRUE.equals(args[0]) && failedOver) {
			createPrimaryConnection();
			
			return super.invoke(proxy, method, args, failedOver);
		} else if ("hashCode".equals(methodName)) {
	        return Integer.valueOf(this.hashCode());
		} else if ("equals".equals(methodName)) {
			if (args[0] instanceof Proxy) {
				return Boolean.valueOf((((Proxy) args[0]).equals(this)));
			}
			return Boolean.valueOf(this.equals(args[0])); 
		}
		return super.invoke(proxy, method, args, failedOver);
	}

	private synchronized void createPrimaryConnection() throws SQLException {
		try {
			this.currentConn = createConnectionForHost(this.primaryHostPortSpec);
			this.failedOver = false;
			this.hasTriedMaster = true;
			
			// reset failed-over state
			this.queriesIssuedFailedOver = 0;
		} catch (SQLException sqlEx) {
			this.failedOver = true;

			if (this.currentConn != null) {
				this.currentConn.getLog().logWarn("Connection to primary host failed", sqlEx);
			}
		}
	}

	synchronized void invalidateCurrentConnection() throws SQLException {
		if (!this.failedOver) {
			this.failedOver = true;
			this.queriesIssuedFailedOver = 0;
			this.masterFailTimeMillis = System.currentTimeMillis();
		}
		super.invalidateCurrentConnection();
	}

	protected synchronized void pickNewConnection() throws SQLException {
		if (this.isClosed && "Connection explicitly closed.".equals(this.closedReason)) {
			return;
		}

		if (this.primaryHostPortSpec == null) {
			this.primaryHostPortSpec = this.hostList.remove(0); // first connect
		}

		if (this.currentConn == null || (this.failedOver && shouldFallBack())) {
			createPrimaryConnection();
			
			if (this.currentConn != null) {
				return;
			}
		}
		
		failOver();
	}

	private synchronized void failOver() throws SQLException {
		if (failedOver) {
			Iterator<Map.Entry<String,ConnectionImpl>> iter = liveConnections.entrySet().iterator();
			
			while (iter.hasNext()) {
				Map.Entry<String,ConnectionImpl> entry = iter.next();
					entry.getValue().close();
			}
			
			liveConnections.clear();
		}
		
		super.pickNewConnection();
		
		if (this.currentConn.getFailOverReadOnly()) {
			this.currentConn.setReadOnly(true);
		} else {
			this.currentConn.setReadOnly(false);
		}
		
		this.failedOver = true;
	}

	/**
	 * Should we try to connect back to the master? We try when we've been
	 * failed over >= this.secondsBeforeRetryMaster _or_ we've issued >
	 * this.queriesIssuedFailedOver
	 * 
	 * @return DOCUMENT ME!
	 */
	private boolean shouldFallBack() {
		long secondsSinceFailedOver = (System.currentTimeMillis() - this.masterFailTimeMillis) / 1000;

		if (secondsSinceFailedOver >= this.secondsBeforeRetryMaster) {
			// reset the timer
			this.masterFailTimeMillis = System.currentTimeMillis();
			
			return true;
		} else if (this.queriesBeforeRetryMaster != 0 && this.queriesIssuedFailedOver >= this.queriesBeforeRetryMaster) {
			return true;
		}
		
		return false;
	}
}
