
/*
 * Copyright 2012 Mario Vernari (http://cetdevelop.codeplex.com/)
 *
 * Licensed 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.
 */

/**
 * 09/Apr/2012:
 *  The original class ModbusTcpCodec has been splitted in two parts,
 *  a base and its derivation. In this way, the base ModbusCodecBase
 *  can be shared for both TCP and RTU mode
 **/

using System;

namespace ModbusLib.Protocols
{
    public class ModbusTcpCodec
        : ModbusCodecBase, IProtocolCodec
    {
        #region IProtocolCodec members

        void IProtocolCodec.ClientEncode(CommDataBase data)
        {
            var client = (ModbusClient)data.OwnerProtocol;
            var command = (ModbusCommand)data.UserData;
            var fncode = command.FunctionCode;

            //encode the command body, if applies
            var body = new ByteArrayWriter();
            var codec = CommandCodecs[fncode];
            if (codec != null)
                codec.ClientEncode(command, body);

            //calculate length field
            var length = 2 + body.Length;

            //create a writer for the outgoing data
            var writer = new ByteArrayWriter();

            //transaction-id 
            writer.WriteUInt16BE((ushort)command.TransId);

            //protocol-identifier (always zero)
            writer.WriteInt16BE(0);

            //message length
            writer.WriteInt16BE((short)length);

            //unit identifier (address)
            writer.WriteByte(client.Address);

            //function code
            writer.WriteByte(fncode);

            //body data
            writer.WriteBytes(body);

            data.OutgoingData = writer.ToReader();
        }



        CommResponse IProtocolCodec.ClientDecode(CommDataBase data)
        {
            var client = (ModbusClient)data.OwnerProtocol;
            var command = (ModbusCommand)data.UserData;
            var incoming = data.IncomingData;

            //validate header first
            if (incoming.Length >= 6 &&
                incoming.ReadUInt16BE() == (ushort)command.TransId &&
                incoming.ReadInt16BE() == 0     //protocol-identifier
                )
            {
                //message length
                var length = incoming.ReadInt16BE();

                //validate address
                if (incoming.Length >= (length + 6) &&
                    incoming.ReadByte() == client.Address
                    )
                {
                    //validate function code
                    var fncode = incoming.ReadByte();

                    if ((fncode & 0x7F) == command.FunctionCode)
                    {
                        if (fncode <= 0x7F)
                        {
                            //
                            var body = new ByteArrayReader(incoming.ReadToEnd());

                            //encode the command body, if applies
                            var codec = CommandCodecs[fncode];
                            if (codec != null)
                                codec.ClientDecode(command, body);

                            return new CommResponse(
                                data,
                                CommResponse.Ack);
                        }
                        else
                        {
                            //exception
                            command.ExceptionCode = incoming.ReadByte();

                            return new CommResponse(
                                data,
                                CommResponse.Critical);
                        }
                    }
                }
            }

            return new CommResponse(
                data,
                CommResponse.Unknown);
        }



        void IProtocolCodec.ServerEncode(CommDataBase data)
        {
            var server = (ModbusServer)data.OwnerProtocol;
            var command = (ModbusCommand)data.UserData;
            var fncode = command.FunctionCode;

            //encode the command body, if applies
            var body = new ByteArrayWriter();
            var codec = CommandCodecs[fncode];
            if (codec != null)
                codec.ServerEncode(command, body);

            //calculate length field
            var length = (command.ExceptionCode == 0)
                ? 2 + body.Length
                : 3;

            //create a writer for the outgoing data
            var writer = new ByteArrayWriter();

            //transaction-id
            writer.WriteUInt16BE((ushort)command.TransId);

            //protocol-identifier
            writer.WriteInt16BE(0);

            //message length
            writer.WriteInt16BE((short)length);

            //unit identifier (address)
            writer.WriteByte(server.Address);

            if (command.ExceptionCode == 0)
            {
                //function code
                writer.WriteByte(command.FunctionCode);

                //body data
                writer.WriteBytes(body);
            }
            else
            {
                //function code
                writer.WriteByte((byte)(command.FunctionCode | 0x80));

                //exception code
                writer.WriteByte(command.ExceptionCode);
            }

            data.OutgoingData = writer.ToReader();
        }



        CommResponse IProtocolCodec.ServerDecode(CommDataBase data)
        {
            var server = (ModbusServer)data.OwnerProtocol;
            var incoming = data.IncomingData;

            //validate header first
            if (incoming.Length < 6)
                goto LabelUnknown;

            //transaction-id
            var transId = incoming.ReadUInt16BE();

            //protocol-identifier
            var protId = incoming.ReadInt16BE();
            if (protId != 0)
                goto LabelIgnore;

            //message length
            var length = incoming.ReadInt16BE();

            if (incoming.Length < (length + 6))
                goto LabelUnknown;

            //address
            var address = incoming.ReadByte();

            if (address == server.Address)
            {
                //function code
                var fncode = incoming.ReadByte();
                if (fncode >= CommandCodecs.Length)
                {
                    throw new ApplicationException("Unknown function code");
                }

                //create a new command
                var command = new ModbusCommand(fncode);
                data.UserData = command;
                command.TransId = transId;

                //
                var body = new ByteArrayReader(incoming.ReadToEnd());

                //encode the command body, if applies
                var codec = CommandCodecs[fncode];
                if (codec != null)
                    codec.ServerDecode(command, body);

                return new CommResponse(
                    data,
                    CommResponse.Ack);
            }

            //exception
        LabelIgnore:
            return new CommResponse(
                data,
                CommResponse.Ignore);

        LabelUnknown:
            return new CommResponse(
                data,
                CommResponse.Unknown);
        }

        #endregion

    }
}
