/*
 * 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.
 */
package org.apache.brooklyn.util.pool;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.util.Deque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Identifiers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;

public class BasicPool<T> implements Pool<T> {

    // TODO Implement expiry of pooled resources

    private static final Logger LOG = LoggerFactory.getLogger(BasicPool.class);

    public static <T> Builder<T> builder() {
        return new Builder<T>();
    }
    
    public static class Builder<T> {
        private String name;
        private Supplier<? extends T> supplier;
        private Predicate<? super T> viabilityChecker = Predicates.alwaysTrue();
        private Function<? super T, ?> closer = Functions.identity();
        
        public Builder<T> name(String val) {
            this.name = val;
            return this;
        }
        
        public Builder<T> supplier(Supplier<? extends T> val) {
            this.supplier = val;
            return this;
        }
        
        public Builder<T> viabilityChecker(Predicate<? super T> val) {
            this.viabilityChecker = val;
            return this;
        }
        
        public Builder<T> closer(Function<? super T, ?> val) {
            this.closer = val;
            return this;
        }
        
        public BasicPool<T> build() {
            return new BasicPool<T>(this);
        }
    }
    
    private final String name;
    private final Supplier<? extends T> supplier;
    private final Predicate<? super T> viabilityChecker;
    private Function<? super T, ?> closer;
    private final Deque<T> pool = Lists.newLinkedList();
    private AtomicBoolean closed = new AtomicBoolean(false);
    
    private AtomicInteger currentLeasedCount = new AtomicInteger(0);
    private AtomicInteger totalLeasedCount = new AtomicInteger(0);
    private AtomicInteger totalCreatedCount = new AtomicInteger(0);
    private AtomicInteger totalClosedCount = new AtomicInteger(0);
    
    private BasicPool(Builder<T> builder) {
        this.name = (builder.name != null) ? "Pool("+builder.name+")" : "Pool-"+Identifiers.makeRandomId(8);
        this.supplier = checkNotNull(builder.supplier, "supplier");
        this.viabilityChecker = checkNotNull(builder.viabilityChecker, "viabilityChecker");
        this.closer = checkNotNull(builder.closer, closer);
    }
    
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("name", name).toString();
    }
    
    @Override
    public Lease<T> leaseObject() {
        totalLeasedCount.incrementAndGet();
        T existing;
        do {
            existing = null;
            synchronized (pool) {
                if (closed.get()) {
                    throw new IllegalStateException("Pool closed for "+this);
                }
                if (pool.size() > 0) {
                    existing = pool.removeLast();
                }
            }
            
            if (existing != null) {
                if (viabilityChecker.apply(existing)) {
                    currentLeasedCount.incrementAndGet();
                    if (LOG.isTraceEnabled()) LOG.trace("{} reusing existing pool entry {} ({})", new Object[] {this, existing, getMetrics()});
                    return new BasicLease(existing);
                } else {
                    totalClosedCount.incrementAndGet();
                    if (LOG.isDebugEnabled()) LOG.debug("{} not reusing entry {} as no longer viable; discarding and trying again", this, existing);
                    closer.apply(existing);
                }
            }
        } while (existing != null);
        
        T result = supplier.get();
        totalCreatedCount.incrementAndGet();
        currentLeasedCount.incrementAndGet();
        if (LOG.isDebugEnabled()) LOG.debug("{} acquired and returning new entry {} ({})", new Object[] {this, result, getMetrics()});
        return new BasicLease(result);
    }

    @Override
    public <R> R exec(Function<? super T,R> receiver) {
        Lease<T> lease = leaseObject();
        try {
            if (LOG.isTraceEnabled()) LOG.trace("{} executing {} with leasee {}", new Object[] {this, receiver, lease.leasedObject()});
            return receiver.apply(lease.leasedObject());
        } finally {
            Streams.closeQuietly(lease);
        }
    }
    
    @Override
    public void close() throws IOException {
        synchronized (pool) {
            if (LOG.isDebugEnabled()) LOG.debug("{} closing, with {} resources ({})", new Object[] {this, pool.size(), getMetrics()});
            closed.set(true);
            for (T resource : pool) {
                totalClosedCount.incrementAndGet();
                closer.apply(resource);
            }
            pool.clear();
        }

    }
    
    private void returnLeasee(T val) {
        currentLeasedCount.decrementAndGet();
        synchronized (pool) {
            if (closed.get()) {
                totalClosedCount.incrementAndGet();
                if (LOG.isDebugEnabled()) LOG.debug("{} closing returned leasee {}, because pool closed ({})", new Object[] {this, val, getMetrics()});
                closer.apply(val);
            } else {
                if (LOG.isTraceEnabled()) LOG.trace("{} adding {} back into pool ({})", new Object[] {this, val, getMetrics()});
                pool.addLast(val);
            }
        }
    }
    
    private String getMetrics() {
        return String.format("currentLeased=%s; totalLeased=%s; totalCreated=%s; totalClosed=%s", 
                currentLeasedCount, totalLeasedCount, totalCreatedCount, totalClosedCount);

    }
    private class BasicLease implements Lease<T> {
        private final T val;

        BasicLease(T val) {
            this.val = val;
        }
        
        @Override
        public T leasedObject() {
            return val;
        }

        @Override
        public void close() {
            BasicPool.this.returnLeasee(val);
        }
    }
}
