/*
 * This file is part of ComputerCraft - http://www.computercraft.info
 * Copyright Daniel Ratcliffe, 2011-2016. Do not distribute without permission.
 * Send enquiries to dratcliffe@gmail.com
 */

package dan200.computercraft.shared.peripheral.modem;

import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;

import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;

public abstract class ModemPeripheral
    implements IPeripheral
{    
    private static class SingleChannelReceiver implements IReceiver
    {
        private ModemPeripheral m_owner;
        private int m_channel;
        
        public SingleChannelReceiver( ModemPeripheral owner, int channel )
        {
            m_owner = owner;
            m_channel = channel;
        }
        
        // IReceiver implementation
        
        @Override
        public int getChannel()
        {
            return m_channel;
        }

        @Override
        public World getWorld()
        {
            return m_owner.getWorld();
        }

        @Override
        public Vec3d getWorldPosition()
        {
            return m_owner.getWorldPosition();
        }
        
        @Override
        public boolean isInterdimensional()
        {
            return m_owner.isInterdimensional();
        }

        @Override
        public double getReceiveRange()
        {
            return m_owner.getReceiveRange();
        }

        @Override
        public void receiveSameDimension( int replyChannel, Object payload, double distance, Object senderObject )
        {
            if( senderObject != m_owner )
            {
                m_owner.receiveSameDimension( m_channel, replyChannel, payload, distance );
            }
        }

        @Override
        public void receiveDifferentDimension( int replyChannel, Object payload, Object senderObject )
        {
            if( senderObject != m_owner )
            {
                m_owner.receiveDifferentDimension( m_channel, replyChannel, payload );
            }
        }
    }
    
    private INetwork m_network;
    private IComputerAccess m_computer;
    private final Map<Integer, IReceiver> m_channels;

    private boolean m_open;
    private boolean m_changed;

    public ModemPeripheral()
    {
        m_network = null;
        m_computer = null;
        
        m_channels = new HashMap<Integer, IReceiver>();
        m_open = false;
        m_changed = true;
    }

    private synchronized void setNetwork( INetwork network )
    {
        if( m_network != network )
        {
            // Leave old network
            if( m_network != null )
            {
                for( IReceiver iReceiver : m_channels.values() )
                {
                    m_network.removeReceiver( iReceiver );
                }
            }

            // Set new network
            m_network = network;

            // Join new network
            if( m_network != null )
            {
                for( IReceiver iReceiver : m_channels.values() )
                {
                    m_network.addReceiver( iReceiver );
                }
            }
        }
    }

    protected void switchNetwork()
    {
        setNetwork( getNetwork() );
    }

    protected abstract World getWorld();

    protected abstract Vec3d getPosition();
        
    public synchronized void destroy()
    {
        setNetwork( null );
        m_channels.clear();
        m_open = false;
    }
    
    public synchronized boolean pollChanged()
    {
        if( m_changed )
        {
            m_changed = false;
            return true;
        }
        return false;
    }
    
    protected abstract double getTransmitRange();

    protected abstract boolean isInterdimensional();

    public synchronized boolean isActive()
    {
        return (m_computer != null) && m_open;
    }

    public synchronized Vec3d getWorldPosition()
    {
        return getPosition();
    }
    
    public synchronized double getReceiveRange()
    {
        return getTransmitRange();
    }
    
    public void receiveSameDimension( int channel, int replyChannel, Object payload, double distance )
    {
        synchronized (m_channels)
        {
            if( m_computer != null && m_channels.containsKey( channel ) )
            {
                m_computer.queueEvent( "modem_message", new Object[] {
                    m_computer.getAttachmentName(), channel, replyChannel, payload, distance
                } );
            }
        }
    }

    public void receiveDifferentDimension( int channel, int replyChannel, Object payload )
    {
        synchronized (m_channels)
        {
            if( m_computer != null && m_channels.containsKey( channel ) )
            {
                m_computer.queueEvent( "modem_message", new Object[] {
                        m_computer.getAttachmentName(), channel, replyChannel, payload
                } );
            }
        }
    }

    protected abstract INetwork getNetwork();
    
    // IPeripheral implementation

    @Nonnull
    @Override
    public String getType()
    {
        return "modem";
    }
       
    @Nonnull
    @Override
    public String[] getMethodNames()
    {
        return new String[] {
            "open",
            "isOpen",
            "close",
            "closeAll",
            "transmit",
            "isWireless",
        };
    }
    
    private static int parseChannel( Object[] arguments, int index ) throws LuaException
    {
        if( arguments.length <= index || !(arguments[index] instanceof Double) )
        {
            throw new LuaException( "Expected number" );
        }
        int channel = (int)((Double)arguments[index]).doubleValue();
        if( channel < 0 || channel > 65535 )
        {
            throw new LuaException( "Expected number in range 0-65535" );
        }
        return channel;
    }
    
    @Override
    public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException
    {
        switch( method )
        {
            case 0:
            {
                // open
                int channel = parseChannel( arguments, 0 );
                synchronized( this )
                {
                    if( !m_channels.containsKey( channel ) )
                    {
                        if( m_channels.size() >= 128 )
                        {
                            throw new LuaException( "Too many open channels" );
                        }
                    
                        IReceiver receiver = new SingleChannelReceiver( this, channel );
                        m_channels.put( channel, receiver );
                        if( m_network != null )
                        {
                            m_network.addReceiver( receiver );
                        }
                        if( !m_open )
                        {
                            m_open = true;
                            m_changed = true;
                        }
                    }
                }
                return null;
            }
            case 1:
            {
                // isOpen
                int channel = parseChannel( arguments, 0 );
                synchronized( this )
                {
                    boolean open = m_channels.containsKey( channel );
                    return new Object[] { open };
                }
            }
            case 2:
            {
                // close
                int channel = parseChannel( arguments, 0 );
                synchronized( this )
                {
                    if( m_channels.containsKey( channel ) )
                    {
                        IReceiver receiver = m_channels.get( channel );
                        if( m_network != null )
                        {
                            m_network.removeReceiver( receiver );
                        }
                        m_channels.remove( channel );
                        
                        if( m_channels.size() == 0 )
                        {
                            m_open = false;
                            m_changed = true;
                        }
                    }
                }
                return null;
            }
            case 3:
            {
                // closeAll
                synchronized( this )
                {
                    if( m_channels.size() > 0 )
                    {
                        if( m_network != null )
                        {
                            for( IReceiver iReceiver : m_channels.values() )
                            {
                                m_network.removeReceiver( iReceiver );
                            }
                        }
                        m_channels.clear();
                        
                        if( m_open )
                        {
                            m_open = false;
                            m_changed = true;
                        }
                    }
                }
                return null;
            }
            case 4:
            {
                // transmit
                int channel = parseChannel( arguments, 0 );
                int replyChannel = parseChannel( arguments, 1 );
                Object payload = (arguments.length >= 3) ? arguments[2] : null;
                synchronized( this )
                {
                    World world = getWorld();
                    Vec3d position = getPosition();
                    if( world != null && position != null && m_network != null)
                    {
                        m_network.transmit( channel, replyChannel, payload, world, position, getTransmitRange(), isInterdimensional(), this );
                    }
                }
                return null;
            }
            case 5:
            {
                // isWireless
                synchronized( this )
                {
                    if( m_network != null )
                    {
                        return new Object[] { m_network.isWireless() };
                    }
                }
                return new Object[] { false };
            }
            default:
            {
                return null;
            }
        }
    }
    
    @Override
    public synchronized void attach( @Nonnull IComputerAccess computer )
    {
        m_computer = computer;
        setNetwork( getNetwork() );
        m_open = !m_channels.isEmpty();
    }
    
    @Override
    public synchronized void detach( @Nonnull IComputerAccess computer )
    {
        if( m_network != null )
        {
            for( IReceiver iReceiver : m_channels.values() )
            {
                m_network.removeReceiver( iReceiver );
            }
            m_channels.clear();
            m_network = null;
        }

        m_computer = null;
            
        if( m_open )
        {
            m_open = false;        
            m_changed = true;
        }
    }

    @Override
    public abstract boolean equals( IPeripheral other );

    public IComputerAccess getComputer()
    {
        return m_computer;
    }
}
