/**
 * server/src/hardware/protocol/xpressnet/messages.hpp
 *
 * This file is part of the traintastic source code.
 *
 * Copyright (C) 2019-2022 Reinder Feenstra
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifndef TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_XPRESSNET_MESSAGES_HPP
#define TRAINTASTIC_SERVER_HARDWARE_PROTOCOL_XPRESSNET_MESSAGES_HPP

#include <cstdint>
#include <cassert>
#include <cstring>
#include <string>
#include "../../../enum/direction.hpp"

namespace XpressNet {

constexpr uint8_t idFeedbackBroadcast = 0x40;

struct Message;

uint8_t calcChecksum(const Message& msg);
void updateChecksum(Message& msg);
bool isChecksumValid(const Message& msg);
std::string toString(const Message& message, bool raw = false);

struct Message
{
  uint8_t header;

  Message()
  {
  }

  Message(uint8_t _header) :
    header{_header}
  {
  }

  constexpr uint8_t identification() const
  {
    return header & 0xF0;
  }

  constexpr uint8_t dataSize() const
  {
    return header & 0x0F;
  }

  constexpr uint8_t size() const
  {
    return 2 + dataSize();
  }
};
static_assert(sizeof(Message) == 1);

struct NormalOperationResumed : Message
{
  uint8_t db1 = 0x01;
  uint8_t checksum = 0x60;

  NormalOperationResumed()
  {
    header = 0x61;
  }
};
static_assert(sizeof(NormalOperationResumed) == 3);

struct TrackPowerOff : Message
{
  uint8_t db1 = 0x00;
  uint8_t checksum = 0x61;

  TrackPowerOff()
  {
    header = 0x61;
  }
};
static_assert(sizeof(TrackPowerOff) == 3);

struct EmergencyStop : Message
{
  uint8_t db1 = 0x00;
  uint8_t checksum = 0x81;

  EmergencyStop()
  {
    header = 0x81;
  }
};
static_assert(sizeof(EmergencyStop) == 3);

struct FeedbackBroadcast : Message
{
  struct Pair
  {
    enum class Type
    {
      AccessoryDecoderWithoutFeedback = 0,
      AccessoryDecoderWithFeedback = 1,
      FeedbackModule = 2,
      ReservedForFutureUse = 3,
    };

    uint8_t address;
    uint8_t data;

    constexpr uint16_t groupAddress() const
    {
      return (static_cast<uint16_t>(address) << 1) | ((data & 0x10) ? 1 : 0);
    }

    void setGroupAddress(uint16_t value)
    {
      assert(value < 512);
      address = value >> 1;
      if(value & 0x0001)
        data |= 0x10;
      else
        data &= 0xEF;
    }

    constexpr bool switchingCommandCompleted() const
    {
      return (data & 0x80);
    }

    constexpr Type type() const
    {
      return static_cast<Type>((data & 0x60) >> 5);
    }

    void setType(Type value)
    {
      assert(
        value == Type::AccessoryDecoderWithoutFeedback ||
        value == Type::AccessoryDecoderWithFeedback ||
        value == Type::FeedbackModule ||
        value == Type::ReservedForFutureUse);

      data = (data & 0x9F) | (static_cast<uint8_t>(value) << 5);
    }

    constexpr uint8_t statusNibble() const
    {
      return (data & 0x0F);
    }

    void setStatus(uint8_t index, bool value)
    {
      assert(index < 4);
      if(value)
        data |= (1 << index);
      else
        data &= ~static_cast<uint8_t>(1 << index);
    }
  };
  static_assert(sizeof(Pair) == 2);

  constexpr uint8_t pairCount() const
  {
    return dataSize() / sizeof(Pair);
  }

  void setPairCount(uint8_t value)
  {
    assert(value <= 7);
    header = (header & 0xF0) | (value * 2);
  }

  const Pair& pair(uint8_t index) const
  {
    assert(index < pairCount());
    return *(reinterpret_cast<const Pair*>(&header + sizeof(header)) + index);
  }

  Pair& pair(uint8_t index)
  {
    assert(index < pairCount());
    return *(reinterpret_cast<Pair*>(&header + sizeof(header)) + index);
  }
};

struct ResumeOperationsRequest : Message
{
  uint8_t db1 = 0x81;
  uint8_t checksum = 0xA0;

  ResumeOperationsRequest()
  {
    header = 0x21;
  }
};
static_assert(sizeof(ResumeOperationsRequest) == 3);

struct StopOperationsRequest : Message
{
  uint8_t db1 = 0x80;
  uint8_t checksum = 0xA1;

  StopOperationsRequest()
  {
    header = 0x21;
  }
};
static_assert(sizeof(StopOperationsRequest) == 3);

struct StopAllLocomotivesRequest : Message
{
  uint8_t checksum = 0x80;

  StopAllLocomotivesRequest()
  {
    header = 0x80;
  }
};
static_assert(sizeof(StopAllLocomotivesRequest) == 2);

struct EmergencyStopLocomotive : Message
{
  uint8_t addressHigh;
  uint8_t addressLow;
  uint8_t checksum;

  EmergencyStopLocomotive(uint16_t address, bool longAddress)
  {
    header = 0x92;
    if(longAddress)
    {
      assert(address >= 1 && address <= 9999);
      addressHigh = 0xC0 | address >> 8;
      addressLow = address & 0xff;
    }
    else
    {
      assert(address >= 1 && address <= 127);
      addressHigh = 0x00;
      addressLow = address & 0x7f;
    }
  }
};
static_assert(sizeof(EmergencyStopLocomotive) == 4);

struct LocomotiveInstruction : Message
{
  uint8_t identification;
  uint8_t addressHigh;
  uint8_t addressLow;

  LocomotiveInstruction(uint16_t address, bool longAddress)
  {
    header = 0xE4;
    if(longAddress)
    {
      assert(address >= 1 && address <= 9999);
      addressHigh = 0xc0 | address >> 8;
      addressLow = address & 0xff;
    }
    else
    {
      assert(address >= 1 && address <= 127);
      addressHigh = 0x00;
      addressLow = address & 0x7f;
    }
  }
};
static_assert(sizeof(LocomotiveInstruction) == 4);

struct SpeedAndDirectionInstruction : LocomotiveInstruction
{
  uint8_t speedAndDirection;
  uint8_t checksum;

  SpeedAndDirectionInstruction(uint16_t address, bool longAddress, bool emergencyStop, Direction direction) :
    LocomotiveInstruction(address, longAddress)
  {
    assert(direction != Direction::Unknown);
    speedAndDirection = emergencyStop ? 0x01 : 0x00;
    if(direction == Direction::Forward)
      speedAndDirection |= 0x80;
  }
};

struct SpeedAndDirectionInstruction14 : SpeedAndDirectionInstruction
{
  SpeedAndDirectionInstruction14(uint16_t address, bool longAddress, bool emergencyStop, Direction direction, uint8_t speedStep, bool fl) :
    SpeedAndDirectionInstruction(address, longAddress, emergencyStop, direction)
  {
    assert(speedStep <= 14);
    identification = 0x10;
    if(!emergencyStop && speedStep > 0)
      speedAndDirection |= speedStep + 1;
    if(fl)
      speedAndDirection |= 0x10;
    checksum = calcChecksum(*this);
  }
};

struct SpeedAndDirectionInstruction27 : SpeedAndDirectionInstruction
{
  SpeedAndDirectionInstruction27(uint16_t address, bool longAddress, bool emergencyStop, Direction direction, uint8_t speedStep) :
    SpeedAndDirectionInstruction(address, longAddress, emergencyStop, direction)
  {
    assert(speedStep <= 27);
    identification = 0x11;
    if(!emergencyStop && speedStep > 0)
      speedAndDirection |= (((speedStep + 1) & 0x01) << 4) | ((speedStep + 1) >> 1);
    checksum = calcChecksum(*this);
  }
};

struct SpeedAndDirectionInstruction28 : SpeedAndDirectionInstruction
{
  SpeedAndDirectionInstruction28(uint16_t address, bool longAddress, bool emergencyStop, Direction direction, uint8_t speedStep) :
    SpeedAndDirectionInstruction(address, longAddress, emergencyStop, direction)
  {
    assert(speedStep <= 28);
    identification = 0x12;
    if(!emergencyStop && speedStep > 0)
      speedAndDirection |= (((speedStep + 1) & 0x01) << 4) | ((speedStep + 1) >> 1);
    checksum = calcChecksum(*this);
  }
};

struct SpeedAndDirectionInstruction128 : SpeedAndDirectionInstruction
{
  SpeedAndDirectionInstruction128(uint16_t address, bool longAddress, bool emergencyStop, Direction direction, uint8_t speedStep) :
    SpeedAndDirectionInstruction(address, longAddress, emergencyStop, direction)
  {
    assert(speedStep <= 126);
    identification = 0x13;
    if(!emergencyStop && speedStep > 0)
      speedAndDirection |= speedStep + 1;
    checksum = calcChecksum(*this);
  }
};

struct FunctionInstructionGroup : LocomotiveInstruction
{
  uint8_t functions = 0x00;
  uint8_t checksum;

  FunctionInstructionGroup(uint16_t address, bool longAddress, uint8_t group) :
    LocomotiveInstruction(address, longAddress)
  {
    assert(group >= 1 && group <= 5);
    identification = (group == 5) ? 0x28 : (0x1F + group);
  }
};

struct FunctionInstructionGroup1 : FunctionInstructionGroup
{
  FunctionInstructionGroup1(uint16_t address, bool longAddress, bool f0, bool f1, bool f2, bool f3, bool f4) :
    FunctionInstructionGroup(address, longAddress, 1)
  {
    if(f0)
      functions |= 0x10;
    if(f1)
      functions |= 0x01;
    if(f2)
      functions |= 0x02;
    if(f3)
      functions |= 0x04;
    if(f4)
      functions |= 0x08;

    checksum = calcChecksum(*this);
  }
};

struct FunctionInstructionGroup2 : FunctionInstructionGroup
{
  FunctionInstructionGroup2(uint16_t address, bool longAddress, bool f5, bool f6, bool f7, bool f8) :
    FunctionInstructionGroup(address, longAddress, 2)
  {
    if(f5)
      functions |= 0x01;
    if(f6)
      functions |= 0x02;
    if(f7)
      functions |= 0x04;
    if(f8)
      functions |= 0x08;

    checksum = calcChecksum(*this);
  }
};

struct FunctionInstructionGroup3 : FunctionInstructionGroup
{
  FunctionInstructionGroup3(uint16_t address, bool longAddress, bool f9, bool f10, bool f11, bool f12) :
    FunctionInstructionGroup(address, longAddress, 3)
  {
    if(f9)
      functions |= 0x01;
    if(f10)
      functions |= 0x02;
    if(f11)
      functions |= 0x04;
    if(f12)
      functions |= 0x08;

    checksum = calcChecksum(*this);
  }
};

struct FunctionInstructionGroup4 : FunctionInstructionGroup
{
  FunctionInstructionGroup4(uint16_t address, bool longAddress, bool f13, bool f14, bool f15, bool f16, bool f17, bool f18, bool f19, bool f20) :
    FunctionInstructionGroup(address, longAddress, 4)
  {
    if(f13)
      functions |= 0x01;
    if(f14)
      functions |= 0x02;
    if(f15)
      functions |= 0x04;
    if(f16)
      functions |= 0x08;
    if(f17)
      functions |= 0x10;
    if(f18)
      functions |= 0x20;
    if(f19)
      functions |= 0x40;
    if(f20)
      functions |= 0x80;

    checksum = calcChecksum(*this);
  }
};

struct FunctionInstructionGroup5 : FunctionInstructionGroup
{
  FunctionInstructionGroup5(uint16_t address, bool longAddress, bool f21, bool f22, bool f23, bool f24, bool f25, bool f26, bool f27, bool f28) :
    FunctionInstructionGroup(address, longAddress, 5)
  {
    if(f21)
      functions |= 0x01;
    if(f22)
      functions |= 0x02;
    if(f23)
      functions |= 0x04;
    if(f24)
      functions |= 0x08;
    if(f25)
      functions |= 0x10;
    if(f26)
      functions |= 0x20;
    if(f27)
      functions |= 0x40;
    if(f28)
      functions |= 0x80;

    checksum = calcChecksum(*this);
  }
};

/*
struct setFunctionStateGroup : LocomotiveInstruction
{
  uint8_t state = 0x00;
  uint8_t checksum;

  setFunctionStateGroup(uint8_t group, uint16_t address)
  {
    assert(group >= 1 && group <= 3);
    header = 0xE4;
    identification = 0x23 + group;
    addressLowHigh(address, addressLow, addressHigh);
  }
} __attribute__((packed));
*/

struct AccessoryDecoderOperationRequest : Message
{
  uint8_t address = 0x00;
  uint8_t data = 0x80;
  uint8_t checksum;

  AccessoryDecoderOperationRequest(uint16_t fullAddress, bool value)
    : Message(0x52)
  {
    assert(fullAddress < 2048);
    address = static_cast<uint8_t>(fullAddress >> 3);
    data |= static_cast<uint8_t>(fullAddress & 0x07);
    if(value)
      data |= 0x40;
    checksum = calcChecksum(*this);
  }
};
static_assert(sizeof(AccessoryDecoderOperationRequest) == 4);

namespace RocoMultiMAUS
{
  struct FunctionInstructionF13F20 : LocomotiveInstruction
  {
    uint8_t functions = 0x00;
    uint8_t checksum;

    FunctionInstructionF13F20(uint16_t address, bool longAddress, bool f13, bool f14, bool f15, bool f16, bool f17, bool f18, bool f19, bool f20) :
      LocomotiveInstruction(address, longAddress)
    {
      identification = 0xF3;

      if(f13)
        functions |= 0x01;
      if(f14)
        functions |= 0x02;
      if(f15)
        functions |= 0x04;
      if(f16)
        functions |= 0x08;
      if(f17)
        functions |= 0x10;
      if(f18)
        functions |= 0x20;
      if(f19)
        functions |= 0x40;
      if(f20)
        functions |= 0x80;

      checksum = calcChecksum(*this);
    }
  };
}

namespace RoSoftS88XpressNetLI
{
  struct S88StartAddress : Message
  {
    static constexpr uint8_t startAddressMin = 0;
    static constexpr uint8_t startAddressMax = 127;
    static constexpr uint8_t startAddressDefault = 64;
    static constexpr uint8_t startAddressGet = 0xFF;

    uint8_t data1;
    uint8_t startAddress;
    uint8_t checksum;

    S88StartAddress(uint8_t _startAddress = startAddressGet) :
      Message(0xF2),
      data1{0xF1},
      startAddress{_startAddress}
    {
      assert((startAddress >= startAddressMin && startAddress <= startAddressMax) || startAddress == startAddressGet);
      checksum = calcChecksum(*this);
    }
  };

  struct S88ModuleCount : Message
  {
    static constexpr uint8_t moduleCountMin = 1;
    static constexpr uint8_t moduleCountMax = 32;
    static constexpr uint8_t moduleCountDefault = 2;
    static constexpr uint8_t moduleCountGet = 0xFF;

    uint8_t data1;
    uint8_t moduleCount;
    uint8_t checksum;

    S88ModuleCount(uint8_t _moduleCount = moduleCountGet) :
      Message(0xF2),
      data1{0xF2},
      moduleCount{_moduleCount}
    {
      assert((moduleCount >= moduleCountMin && moduleCount <= moduleCountMax) || moduleCount == moduleCountGet);
      checksum = calcChecksum(*this);
    }
  };
}

}

inline bool operator ==(const XpressNet::Message& lhs, const XpressNet::Message& rhs)
{
  return lhs.size() == rhs.size() && std::memcmp(&lhs, &rhs, lhs.size()) == 0;
}

#endif
