//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.client;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Sweeper;

public class MultiplexConnectionPool extends AbstractConnectionPool implements ConnectionPool.Multiplexable, Sweeper.Sweepable
{
    private static final Logger LOG = Log.getLogger(MultiplexConnectionPool.class);

    private final ReentrantLock lock = new ReentrantLock();
    private final HttpDestination destination;
    private final Deque<Holder> idleConnections;
    private final Map<Connection, Holder> muxedConnections;
    private final Map<Connection, Holder> busyConnections;
    private int maxMultiplex;

    public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex)
    {
        super(destination, maxConnections, requester);
        this.destination = destination;
        this.idleConnections = new ArrayDeque<>(maxConnections);
        this.muxedConnections = new HashMap<>(maxConnections);
        this.busyConnections = new HashMap<>(maxConnections);
        this.maxMultiplex = maxMultiplex;
    }

    @Override
    public Connection acquire()
    {
        Connection connection = activate();
        if (connection == null)
        {
            int maxPending = 1 + destination.getQueuedRequestCount() / getMaxMultiplex();
            tryCreate(maxPending);
            connection = activate();
        }
        return connection;
    }

    protected void lock()
    {
        lock.lock();
    }

    protected void unlock()
    {
        lock.unlock();
    }

    @Override
    public int getMaxMultiplex()
    {
        lock();
        try
        {
            return maxMultiplex;
        }
        finally
        {
            unlock();
        }
    }

    @Override
    public void setMaxMultiplex(int maxMultiplex)
    {
        lock();
        try
        {
            this.maxMultiplex = maxMultiplex;
        }
        finally
        {
            unlock();
        }
    }

    @Override
    public boolean isActive(Connection connection)
    {
        lock();
        try
        {
            if (muxedConnections.containsKey(connection))
                return true;
            return busyConnections.containsKey(connection);
        }
        finally
        {
            unlock();
        }
    }

    @Override
    protected void onCreated(Connection connection)
    {
        lock();
        try
        {
            // Use "cold" connections as last.
            idleConnections.offer(new Holder(connection));
        }
        finally
        {
            unlock();
        }

        idle(connection, false);
    }

    @Override
    protected Connection activate()
    {
        Holder holder;
        lock();
        try
        {
            while (true)
            {
                if (muxedConnections.isEmpty())
                {
                    holder = idleConnections.poll();
                    if (holder == null)
                        return null;
                    muxedConnections.put(holder.connection, holder);
                }
                else
                {
                    holder = muxedConnections.values().iterator().next();
                }

                if (holder.count < maxMultiplex)
                {
                    ++holder.count;
                    break;
                }
                else
                {
                    muxedConnections.remove(holder.connection);
                    busyConnections.put(holder.connection, holder);
                }
            }
        }
        finally
        {
            unlock();
        }

        return active(holder.connection);
    }

    @Override
    public boolean release(Connection connection)
    {
        boolean closed = isClosed();
        boolean idle = false;
        Holder holder;
        lock();
        try
        {
            holder = muxedConnections.get(connection);
            if (holder != null)
            {
                int count = --holder.count;
                if (count == 0)
                {
                    muxedConnections.remove(connection);
                    if (!closed)
                    {
                        idleConnections.offerFirst(holder);
                        idle = true;
                    }
                }
            }
            else
            {
                holder = busyConnections.remove(connection);
                if (holder != null)
                {
                    int count = --holder.count;
                    if (!closed)
                    {
                        if (count == 0)
                        {
                            idleConnections.offerFirst(holder);
                            idle = true;
                        }
                        else
                        {
                            muxedConnections.put(connection, holder);
                        }
                    }
                }
            }
        }
        finally
        {
            unlock();
        }

        if (holder == null)
            return false;

        released(connection);
        if (idle || closed)
            return idle(connection, closed);
        return true;
    }

    @Override
    public boolean remove(Connection connection)
    {
        return remove(connection, false);
    }

    protected boolean remove(Connection connection, boolean force)
    {
        boolean activeRemoved = true;
        boolean idleRemoved = false;
        lock();
        try
        {
            Holder holder = muxedConnections.remove(connection);
            if (holder == null)
                holder = busyConnections.remove(connection);
            if (holder == null)
            {
                activeRemoved = false;
                for (Iterator<Holder> iterator = idleConnections.iterator(); iterator.hasNext(); )
                {
                    holder = iterator.next();
                    if (holder.connection == connection)
                    {
                        idleRemoved = true;
                        iterator.remove();
                        break;
                    }
                }
            }
        }
        finally
        {
            unlock();
        }

        if (activeRemoved || force)
            released(connection);
        boolean removed = activeRemoved || idleRemoved || force;
        if (removed)
            removed(connection);
        return removed;
    }

    @Override
    public void close()
    {
        super.close();

        List<Connection> connections;
        lock();
        try
        {
            connections = idleConnections.stream().map(holder -> holder.connection).collect(Collectors.toList());
            connections.addAll(muxedConnections.keySet());
            connections.addAll(busyConnections.keySet());
        }
        finally
        {
            unlock();
        }

        close(connections);
    }

    @Override
    public void dump(Appendable out, String indent) throws IOException
    {
        DumpableCollection busy;
        DumpableCollection muxed;
        DumpableCollection idle;
        lock();
        try
        {
            busy = new DumpableCollection("busy", new ArrayList<>(busyConnections.values()));
            muxed = new DumpableCollection("muxed", new ArrayList<>(muxedConnections.values()));
            idle = new DumpableCollection("idle", new ArrayList<>(idleConnections));
        }
        finally
        {
            unlock();
        }

        Dumpable.dumpObjects(out, indent, this, busy, muxed, idle);
    }

    @Override
    public boolean sweep()
    {
        List<Connection> toSweep = new ArrayList<>();
        lock();
        try
        {
            busyConnections.values().stream()
                .map(holder -> holder.connection)
                .filter(connection -> connection instanceof Sweeper.Sweepable)
                .collect(Collectors.toCollection(() -> toSweep));
            muxedConnections.values().stream()
                .map(holder -> holder.connection)
                .filter(connection -> connection instanceof Sweeper.Sweepable)
                .collect(Collectors.toCollection(() -> toSweep));
        }
        finally
        {
            unlock();
        }

        for (Connection connection : toSweep)
        {
            if (((Sweeper.Sweepable)connection).sweep())
            {
                boolean removed = remove(connection, true);
                LOG.warn("Connection swept: {}{}{} from active connections{}{}",
                    connection,
                    System.lineSeparator(),
                    removed ? "Removed" : "Not removed",
                    System.lineSeparator(),
                    dump());
            }
        }

        return false;
    }

    @Override
    public String toString()
    {
        int busySize;
        int muxedSize;
        int idleSize;
        lock();
        try
        {
            busySize = busyConnections.size();
            muxedSize = muxedConnections.size();
            idleSize = idleConnections.size();
        }
        finally
        {
            unlock();
        }
        return String.format("%s@%x[c=%d/%d/%d,b=%d,m=%d,i=%d]",
            getClass().getSimpleName(),
            hashCode(),
            getPendingConnectionCount(),
            getConnectionCount(),
            getMaxConnectionCount(),
            busySize,
            muxedSize,
            idleSize);
    }

    private static class Holder
    {
        private final Connection connection;
        private int count;

        private Holder(Connection connection)
        {
            this.connection = connection;
        }

        @Override
        public String toString()
        {
            return String.format("%s[%d]", connection, count);
        }
    }
}
