/*
 * Copyright (c) 2017 XMLmind Software. All rights reserved. 
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind W2X project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.w2x_servlet;

import java.io.IOException;
import java.io.File;
import java.util.LinkedList;
import java.util.HashMap;
import java.util.TimeZone;
import java.text.DateFormat;
import java.net.UnknownHostException;
import java.net.InetAddress;
import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.StringUtil;

/*package*/ final class RequestLimiter {
    private final File baseDir;

    private int maxRequestCount;
    private int maxRequestPeriod;
    private long maxRequestPeriodMS;
    private InetAddress[] unrestrictedAddrs;

    private static final class Limit {
        public final InetAddress requestor;
        public final long expires;
        public int remaining;

        public Limit(InetAddress requestor, long expires, int remaining) {
            this.requestor = requestor;
            this.expires = expires;
            this.remaining = remaining;
        }
    }

    private LinkedList<Limit> limitList;
    private HashMap<InetAddress,Limit> limits;

    // -----------------------------------------------------------------------

    public RequestLimiter(ServletConfig config, File baseDir) 
        throws ServletException {
        this.baseDir = baseDir;

        maxRequestCount = 
            ConfUtil.getInt(config, "maxRequestCount", 
                            "0", Integer.MIN_VALUE, Integer.MAX_VALUE);
        maxRequestPeriod = 
            ConfUtil.getInt(config, "maxRequestPeriod", 
                            "0", Integer.MIN_VALUE, Integer.MAX_VALUE);

        if (maxRequestCount > 0 && maxRequestPeriod > 0) {
            limitList = new LinkedList<Limit>();
            limits = new HashMap<InetAddress,Limit>();

            maxRequestPeriodMS = maxRequestPeriod * 3600L * 1000L;

            String addrs = ConfUtil.getString(config, "unrestrictedHosts", "");
            if (addrs.length() == 0) {
                unrestrictedAddrs = new InetAddress[0];
            } else {
                String[] addrList = StringUtil.split(addrs);
                unrestrictedAddrs = new InetAddress[addrList.length];

                for (int i = 0; i < addrList.length; ++i) {
                    try {
                        unrestrictedAddrs[i] = 
                            InetAddress.getByName(addrList[i]);
                    } catch (UnknownHostException e) {
                        ConfUtil.invalidParameterValue2(addrList[i], 
                                                       "unrestrictedHosts",
                                                       ThrowableUtil.reason(e));
                    }
                }
            }
        }
    }

    public synchronized boolean allowRequest(HttpServletRequest req,
                                             boolean interactive,
                                             HttpServletResponse resp) 
        throws IOException {
        if (limits == null) {
            // No limits.
            return true;
        }

        InetAddress addr = null;
        String host = req.getRemoteAddr();
        if (host != null) {
            try {
                addr = InetAddress.getByName(host);
            } catch (UnknownHostException shouldNotHappen) {}
        }
        if (addr == null) {
            String msg = Msg.msg("noRemoteAddr");
            if (interactive) {
                RespUtil.sendAlert("error", msg, null, baseDir, resp);
            } else {
                resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                               msg);
            }
            return false;
        }

        // ---

        if (addr.isLoopbackAddress() || addr.isSiteLocalAddress()) {
            return true;
        }

        for (InetAddress unrestrictedAddr : unrestrictedAddrs) {
            if (addr.equals(unrestrictedAddr)) {
                return true;
            }
        }

        // ---

        long now = System.currentTimeMillis();

        Limit limit;
        while ((limit = limitList.peek()) != null) {
            if (limit.expires > now) {
                break;
            }

            limitList.remove();
            limits.remove(limit.requestor);
        }

        // ---

        limit = limits.get(addr);
        if (limit == null) {
            limit =
                new Limit(addr, now + maxRequestPeriodMS, maxRequestCount - 1);
            limitList.add(limit);
            limits.put(addr, limit);
        } else {
            if (limit.remaining <= 0) {
                String msg =
                    Msg.msg("limitReached", 
                            maxRequestCount, maxRequestPeriod, 
                            addr.getHostAddress(), 
                            formatDate(limit.expires - maxRequestPeriodMS), 
                            formatDate(limit.expires));

                if (interactive) {
                    RespUtil.sendAlert("information", msg, null, baseDir, resp);
                } else {
                    resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                   msg);
                }
                return false;
            }

            --limit.remaining;
        }

        return true;
    }

    // Same locale as the locale of the messages.
    private static DateFormat DATE_FORMAT = 
        DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
    static {
        DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    private static final String formatDate(long date) {
        return DATE_FORMAT.format(date);
    }
}

