// Big Screen Bot
//
// Douglas Thrift
//
// BigScreenBot.java

/*  Copyright 2011 Douglas Thrift
 *
 *  This file is part of Big Screen Bot.
 *
 *  Big Screen Bot 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, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Big Screen Bot 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 Big Screen Bot.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.douglasthrift.bigscreenbot;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import java.net.InetAddress;
import java.net.UnknownHostException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import java.util.regex.Pattern;

import javax.net.ssl.SSLSocketFactory;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;

import com.google.anymote.common.AnymoteFactory;
import com.google.anymote.common.ErrorListener;

import com.google.anymote.device.DeviceAdapter;
import com.google.anymote.device.MessageReceiver;

import org.apache.oro.text.GlobCompiler;

import org.jibble.pircbot.Colors;
import org.jibble.pircbot.IrcException;
import org.jibble.pircbot.PircBot;

public class BigScreenBot extends PircBot
{
    private static final String SETTINGS = "bigscreenbot.properties";
    private static final int CHANNEL = 0x1;
    private static final int PRIVATE = 0x2;
    private static final int BOTH    = 0x3;

    private static enum Action { RECONNECT, RESTART, QUIT }

    private static class Settings extends Properties
    {
        public boolean getBooleanProperty(String key, boolean defaultValue)
        {
            String value = getProperty(key);

            if (value != null)
                return Boolean.parseBoolean(value);
            else
                return defaultValue;
        }

        public int getIntegerProperty(String key, int defaultValue)
        {
            String value = getProperty(key);

            if (value != null)
                return Integer.parseInt(value);
            else
                return defaultValue;
        }

        public List<String> getListProperty(String key)
        {
            String value = getProperty(key);

            if (value != null)
                return Arrays.asList(value.split("\\s"));
            else
                return null;
        }
    }

    private abstract class Command
    {
        private boolean admin;
        private int access;
        private String arguments, description;

        protected Command(boolean admin, int access, String arguments, String description)
        {
            this.admin = admin;
            this.access = access;
            this.arguments = arguments;
            this.description = description;
        }

        public abstract void execute(String channel, String sender, boolean admin, String argument);

        public int getAccess()
        {
            return access;
        }

        public String getArguments()
        {
            return arguments;
        }

        public String getDescription()
        {
            return description;
        }

        public boolean isAdmin()
        {
            return admin;
        }

        public boolean isChannel()
        {
            return (access & CHANNEL) != 0;
        }

        public boolean isPrivate()
        {
            return (access & PRIVATE) != 0;
        }
    }

    private Settings settings;
    private List<Pattern> admins = new ArrayList<Pattern>();
    private Map<String, Command> commands = new TreeMap<String, Command>();
    private Action action = Action.RECONNECT;

    private BigScreenBot(Settings settings)
    {
        super();

        this.settings = settings;

        setAutoNickChange(true);
        setFinger("Big Screen Bot");
        setMessageDelay(0);
        setVersion("Big Screen Bot (" + System.getProperty("os.name") + ")");

        if (settings.getBooleanProperty("ssl", false))
        {
            // TODO: figure out how to make this work with Jay's certificate
            setSocketFactory(SSLSocketFactory.getDefault());
        }

        setLogin(System.getProperty("user.name"));
        setName(settings.getProperty("nick", "bigscreenbot"));
        setVerbose(settings.getBooleanProperty("verbose", false));

        for (String admin : settings.getListProperty("admins"))
            admins.add(Pattern.compile(GlobCompiler.globToPerl5(admin.toCharArray(), GlobCompiler.DEFAULT_MASK), Pattern.CASE_INSENSITIVE));

        commands.put("fling", new Command(false, BOTH, "url [device]", "fling url to a Google TV device")
        {
            @Override
            public void execute(String channel, String sender, boolean admin, String argument)
            {
            }
        });
        commands.put("help", new Command(false, PRIVATE, "[command]", "show this help message")
        {
            @Override
            public void execute(String channel, String sender, boolean admin, String arguments)
            {
                String argument = arguments.split("\\s", 2)[0].toLowerCase();

                if (argument.startsWith("!"))
                    argument = argument.substring(1);

                Command command = commands.get(argument);

                sendMessage(channel, sender, Colors.BOLD + "command     arguments               access          description" + Colors.NORMAL);

                if (command != null)
                    help(channel, sender, admin, argument, command);
                else
                    for (Map.Entry<String, Command> nameCommand : commands.entrySet())
                        help(channel, sender, admin, nameCommand.getKey(), nameCommand.getValue());
            }

            private void help(String channel, String sender, boolean admin, String name, Command command)
            {
                boolean unavailable = command.isAdmin() && !admin;
                String access;

                switch (command.getAccess())
                {
                case CHANNEL:
                    access = "channel"; break;
                case PRIVATE:
                    access = "private"; break;
                case BOTH: default:
                    access = "channel/private"; break;
                }

                sendMessage(channel, sender, (unavailable ? Colors.UNDERLINE : "") + String.format("%1$-11s %2$-23s %3$-15s %4$s", name, command.getArguments(), access, command.getDescription()) + (unavailable ? Colors.NORMAL : ""));
            }
        });
        commands.put("pair", new Command(true, PRIVATE, "[device [code]]", "pair with a Google TV device")
        {
            @Override
            public void execute(String channel, String sender, boolean admin, String argument)
            {
            }
        });
        commands.put("quit", new Command(true, PRIVATE, "[message]", "quit and do not come back")
        {
            @Override
            public void execute(String channel, String sender, boolean admin, String argument)
            {
                synchronized (action)
                {
                    action = Action.QUIT;
                }

                if (!argument.isEmpty())
                    quitServer(argument);
                else
                    quitServer();
            }
        });
        commands.put("restart", new Command(true, PRIVATE, "", "quit and join running more up to date code")
        {
            @Override
            public void execute(String channel, String sender, boolean admin, String argument)
            {
                synchronized (action)
                {
                    action = Action.RESTART;
                }

                quitServer("restarting");
            }
        });
        commands.put("say", new Command(true, PRIVATE, "nick|channel message", "say message to nick or channel")
        {
            @Override
            public void execute(String channel, String sender, boolean admin, String argument)
            {
                String[] arguments = argument.split("\\s", 2);

                if (arguments.length != 2)
                {
                    help(channel, sender, admin, "say");

                    return;
                }

                if (arguments[0].equalsIgnoreCase(getNick()))
                {
                    sendMessage(channel, sender, "nice try");

                    return;
                }

                sendMessage(arguments[0], arguments[1]);
                sendMessage(channel, sender, String.format("successfully sent message (\"%1$s\") to nick/channel (\"%2$s\")", arguments[1], arguments[0]));
            }
        });

        try
        {
            connect(settings.getProperty("server"), settings.getIntegerProperty("port", 6667));
        }
        catch (IOException exception)
        {
            error(exception, 1);
        }
        catch (IrcException exception)
        {
            error(exception, 1);
        }
    }

    public void sendMessage(String channel, String sender, String message)
    {
        sendMessage(channel != null ? channel : sender, message);
    }

    @Override
    protected void onConnect()
    {
        joinChannel("#bigscreenbot");
    }

    @Override
    protected void onDisconnect()
    {
        synchronized (action)
        {
            switch (action)
            {
            case RESTART:
                dispose();

                System.exit(2);
            case QUIT:
                dispose();
                break;
            default:
                while (true)
                {
                    try
                    {
                        reconnect();
                    }
                    catch (IOException exception)
                    {
                        continue;
                    }
                    catch (IrcException exception)
                    {
                        error(exception, 1);
                    }

                    break;
                }
            }
        }
    }

    @Override
    protected void onMessage(String channel, String sender, String login, String hostname, String message)
    {
        message = Colors.removeFormattingAndColors(message);

        doCommandFromMessage(channel, sender, login, hostname, message);
    }

    @Override
    protected void onPrivateMessage(String sender, String login, String hostname, String message)
    {
        message = Colors.removeFormattingAndColors(message);

        doCommandFromMessage(null, sender, login, hostname, message);
    }

    private void doCommandFromMessage(String channel, String sender, String login, String hostname, String message)
    {
        boolean admin = matchNickMasks(sender, login, hostname, admins);

        // TODO: determine if this person is in our channels before responding

        String[] arguments = message.split("\\s", 2);
        String argument = arguments[0].toLowerCase();

        if (argument.startsWith("!"))
            argument = argument.substring(1);

        Command command = commands.get(argument);

        if (channel != null)
        {
            if (command == null)
                return;

            if (!admin && command.isAdmin())
                return;

            if (!command.isChannel())
                return;
        }
        else
        {
            if (command == null)
            {
                sendMessage(channel, sender, "unknown command (\"" + argument + "\"); try \"help\"");

                return;
            }

            if (!admin && command.isAdmin())
            {
                sendMessage(channel, sender, "unauthorized command (\"" + argument + "\")");

                return;
            }

            if (!command.isPrivate())
            {
                sendMessage(channel, sender, "inappropriate command (\"" + argument + "\")");

                return;
            }
        }

        command.execute(channel, sender, admin, arguments.length == 2 ? arguments[1] : "");
    }

    private void help(String channel, String sender, boolean admin, String command)
    {
        commands.get("help").execute(channel, sender, admin, command);
    }

    public static void main(String[] args)
    {
        /*JmDNS dns = JmDNS.create(InetAddress.getByName("10.0.0.1"));

        for (ServiceInfo info : dns.list("_anymote._tcp.local."))
        {
            System.out.println(info.getName());
            System.out.println(info.getInetAddresses()[0]);
            System.out.println(info.getPort());
        }

        dns.close();*/

        Settings settings = new Settings();
        FileReader reader = null;

        try
        {
            reader = new FileReader(SETTINGS);

            settings.load(reader);
        }
        catch (FileNotFoundException exception)
        {
            error(exception, 1);
        }
        catch (IOException exception)
        {
            error(exception, 1);
        }
        finally
        {
            if (reader != null)
                try
                {
                    reader.close();
                }
                catch (IOException exception)
                {
                    error(exception, 1);
                }
        }

        new BigScreenBot(settings);
    }

    private static void error(Exception exception)
    {
        System.err.println("bigscreenbot: " + exception.getMessage());
    }

    private static void error(Exception exception, int code)
    {
        error(exception);

        System.exit(code);
    }

    private static boolean matchNickMasks(String sender, String login, String hostname, List<Pattern> masks)
    {
        String nick = sender + "!" + login + "@" + hostname;

        for (Pattern mask : masks)
            if (mask.matcher(nick).matches())
                return true;

        try
        {
            InetAddress address = InetAddress.getByName(hostname);
            String name = address.getCanonicalHostName();

            if (!name.equalsIgnoreCase(hostname))
            {
                nick = sender + "!" + login + "@" + name;

                for (Pattern mask : masks)
                    if (mask.matcher(nick).matches())
                        return true;
            }
        }
        catch (UnknownHostException exception) {}

        return false;
    }
}

// vim: expandtab
