/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */
using System;
using System.Text;
using log4net;
using Qpid.Client.Qms.Failover;

namespace Qpid.Client.Qms
{
    public class FailoverPolicy
    {
        private static readonly ILog _logger = LogManager.GetLogger(typeof(FailoverPolicy));

        private const long MINUTE = 60000L;

        private const long DEFAULT_METHOD_TIMEOUT = 1 * MINUTE;
        private const long DEFAULT_FAILOVER_TIMEOUT = 4 * MINUTE;

        private IFailoverMethod[] _methods = new IFailoverMethod[1];

        private int _currentMethod;

        private int _methodsRetries;

        private int _currentRetry;

        private bool _timing;

        private long _lastMethodTime;
        private long _lastFailTime;

        public FailoverPolicy(IConnectionInfo connectionInfo)
        {
            IFailoverMethod method;

            //todo This should be integrated in to the connection url when it supports
            // multiple strategies.

            _methodsRetries = 0;

            if (connectionInfo.FailoverMethod == null)
            {
                if (connectionInfo.BrokerCount > 1)
                {
                    method = new FailoverRoundRobin(connectionInfo);
                }
                else
                {
                    method = new FailoverSingleServer(connectionInfo);
                }
            }
            else
            {
                string failoverMethod = connectionInfo.FailoverMethod;

    /*
                if (failoverMethod.equals(FailoverMethod.RANDOM))
                {
                    //todo write a random connection Failover
                }
    */
                if (failoverMethod.Equals(FailoverMethodConstants.ROUND_ROBIN))
                {
                    method = new FailoverRoundRobin(connectionInfo);
                }
                else
                {
                    throw new NotImplementedException("Dynamic loading of FailoverMethods not yet implemented.");
//                    try
//                    {
//                        Type[] constructorSpec = {ConnectionInfo.class};
//                        Object [] params = {connectionInfo};
//
//                        method = (FailoverMethod) ClassLoader.getSystemClassLoader().
//                                loadClass(failoverMethod).
//                                getConstructor(constructorSpec).newInstance(params);
//                    }
//                    catch (Exception cnfe)
//                    {
//                        throw new IllegalArgumentException("Unknown failover method:" + failoverMethod);
//                    }
                }
            }

            if (method == null)
            {
                throw new ArgumentException("Unknown failover method specified.");
            }

            reset();

            _methods[_currentMethod] = method;
        }

        public FailoverPolicy(IFailoverMethod method) : this(method, 0)
        {
        }

        public FailoverPolicy(IFailoverMethod method, int retries)
        {
            _methodsRetries = retries;

            reset();

            _methods[_currentMethod] = method;
        }

        private void reset()
        {
            _currentMethod = 0;
            _currentRetry = 0;
            _timing = false;

        }

        public bool FailoverAllowed()
        {
            bool failoverAllowed;

            if (_timing)
            {
                long now = CurrentTimeMilliseconds();

                if ((now - _lastMethodTime) >= DEFAULT_METHOD_TIMEOUT)
                {
                    _logger.Info("Failover method timeout");
                    _lastMethodTime = now;

                    if (!nextMethod())
                    {
                        return false;
                    }


                }
                else if ((now - _lastFailTime) >= DEFAULT_FAILOVER_TIMEOUT)
                {
                    _logger.Info("Failover timeout");
                    return false;
                }
                else
                {
                    _lastMethodTime = now;
                }
            }
            else
            {
                _timing = true;
                _lastMethodTime = CurrentTimeMilliseconds();
                _lastFailTime = _lastMethodTime;
            }


            if (_methods[_currentMethod].FailoverAllowed())
            {
                failoverAllowed = true;
            }
            else
            {
                if (_currentMethod < (_methods.Length - 1))
                {
                    nextMethod();
                    _logger.Info("Changing method to " + _methods[_currentMethod].MethodName);
                    return FailoverAllowed();
                }
                else
                {
                    return cycleMethods();
                }
            }

            return failoverAllowed;
        }

        private static long CurrentTimeMilliseconds()
        {
            return DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
        }

        private bool nextMethod()
        {
            if (_currentMethod < (_methods.Length - 1))
            {
                _currentMethod++;
                _methods[_currentMethod].Reset();
                return true;
            }
            else
            {
                return cycleMethods();
            }
        }

        private bool cycleMethods()
        {
            if (_currentRetry < _methodsRetries)
            {
                _currentRetry++;

                _currentMethod = 0;

                _logger.Info("Retrying methods starting with " + _methods[_currentMethod].MethodName);
                _methods[_currentMethod].Reset();
                return FailoverAllowed();
            }
            else
            {
                _logger.Debug("All failover methods exhausted");
                return false;
            }
        }

        /**
         * Notification that connection was successful.
         */
        public void attainedConnection()
        {
            _currentRetry = 0;

            _methods[_currentMethod].AttainedConnection();

            _timing = false;
        }

        public IBrokerInfo GetCurrentBrokerInfo()
        {
            return _methods[_currentMethod].GetCurrentBrokerInfo();
        }

        public IBrokerInfo GetNextBrokerInfo()
        {
            return _methods[_currentMethod].GetNextBrokerDetails();
        }

        public void setBroker(IBrokerInfo broker)
        {
            _methods[_currentMethod].SetBroker(broker);
        }

        public void addMethod(IFailoverMethod method)
        {
            int len = _methods.Length + 1;
            IFailoverMethod[] newMethods = new IFailoverMethod[len];
            _methods.CopyTo(newMethods, 0);
//            System.arraycopy(_methods, 0, newMethods, 0, _methods.length);
            int index = len - 1;
            newMethods[index] = method;
            _methods = newMethods;
        }

        public void setMethodRetries(int retries)
        {
            _methodsRetries = retries;
        }

        public IFailoverMethod getCurrentMethod()
        {
            if (_currentMethod >= 0 && _currentMethod < (_methods.Length - 1))
            {
                return _methods[_currentMethod];
            }
            else
            {
                return null;
            }
        }

        public String toString()
        {
            StringBuilder sb = new StringBuilder();

            sb.Append("Failover Policy:\n");

            if (FailoverAllowed())
            {
                sb.Append("Failover allowed\n");
            }
            else
            {
                sb.Append("Failover not allowed\n");
            }

            sb.Append("Failover policy methods\n");
            for (int i = 0; i < _methods.Length; i++)
            {

                if (i == _currentMethod)
                {
                    sb.Append(">");
                }
                sb.Append(_methods[i].ToString());
            }

            return sb.ToString();
        }
    }
}
