//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2017 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    as published by the Free Software Foundation.
//
//    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 Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.
//

#pragma once

#include <Windows.h>

#include <string>
#include <type_traits>

#include <openvpn/buffer/bufhex.hpp>
#include <openvpn/common/rc.hpp>
#include <openvpn/win/scoped_handle.hpp>
#include <openvpn/win/event.hpp>

#define TUN_IOCTL_REGISTER_RINGS CTL_CODE(51820U, 0x970U, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA)
#define TUN_IOCTL_FORCE_CLOSE_HANDLES CTL_CODE(51820U, 0x971U, METHOD_NEITHER, FILE_READ_DATA | FILE_WRITE_DATA)

#define WINTUN_RING_CAPACITY 0x800000
#define WINTUN_RING_TRAILING_BYTES 0x10000
#define WINTUN_RING_FRAMING_SIZE 12
#define WINTUN_MAX_PACKET_SIZE 0xffff
#define WINTUN_PACKET_ALIGN 4

namespace openvpn
{
  namespace TunWin
  {
    struct TUN_RING {
      std::atomic_ulong head;
      std::atomic_ulong tail;
      std::atomic_long alertable;
      UCHAR data[WINTUN_RING_CAPACITY + WINTUN_RING_TRAILING_BYTES + WINTUN_RING_FRAMING_SIZE];
    };

    struct TUN_REGISTER_RINGS
    {
      struct
      {
	ULONG ring_size;
	TUN_RING* ring;
	HANDLE tail_moved;
      } send, receive;
    };

    typedef openvpn_io::windows::object_handle AsioEvent;

    class RingBuffer : public RC<thread_unsafe_refcount>
    {
    public:
      typedef RCPtr<RingBuffer> Ptr;

      RingBuffer(openvpn_io::io_context& io_context)
	: send_ring_hmem(CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(TUN_RING), NULL)),
	  receive_ring_hmem(CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(TUN_RING), NULL)),
	  send_tail_moved_asio_event_(io_context)
      {
	// sanity checks
	static_assert((sizeof(TUN_RING) - sizeof(TUN_RING::data)) == 12, "sizeof(TUN_RING) is expected to be 12");
#if !defined(ATOMIC_LONG_LOCK_FREE) || (ATOMIC_LONG_LOCK_FREE != 2)
#error Atomic long is expected to be always lock-free
#endif

	send_ring_ = (TUN_RING*)MapViewOfFile(send_ring_hmem(), FILE_MAP_ALL_ACCESS, 0, 0, sizeof(TUN_RING));
	receive_ring_ = (TUN_RING*)MapViewOfFile(receive_ring_hmem(), FILE_MAP_ALL_ACCESS, 0, 0, sizeof(TUN_RING));
	send_tail_moved_asio_event_.assign(send_ring_tail_moved_());
      }

      RingBuffer(openvpn_io::io_context& io_context,
		 HANDLE client_process,
		 const std::string& send_ring_hmem_hex,
		 const std::string& receive_ring_hmem_hex,
		 const std::string& send_ring_tail_moved_hex,
		 const std::string& receive_ring_tail_moved_hex)
	: send_tail_moved_asio_event_(io_context)
      {
	HANDLE remote_handle = BufHex::parse<HANDLE>(send_ring_hmem_hex, "send_ring_hmem");
	HANDLE handle;
	DuplicateHandle(client_process, remote_handle, GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS);
	send_ring_hmem.reset(handle);

	remote_handle = BufHex::parse<HANDLE>(receive_ring_hmem_hex, "receive_ring_hmem");
	DuplicateHandle(client_process, remote_handle, GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS);
	receive_ring_hmem.reset(handle);

	remote_handle = BufHex::parse<HANDLE>(send_ring_tail_moved_hex, "send_ring_tail_moved");
	DuplicateHandle(client_process, remote_handle, GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS);
	send_ring_tail_moved_.reset(handle);

	remote_handle = BufHex::parse<HANDLE>(receive_ring_tail_moved_hex, "receive_ring_tail_moved");
	DuplicateHandle(client_process, remote_handle, GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS);
	receive_ring_tail_moved_.reset(handle);

	send_ring_ = (TUN_RING*)MapViewOfFile(send_ring_hmem(), FILE_MAP_ALL_ACCESS, 0, 0, sizeof(TUN_RING));
	receive_ring_ = (TUN_RING*)MapViewOfFile(receive_ring_hmem(), FILE_MAP_ALL_ACCESS, 0, 0, sizeof(TUN_RING));
      }

      RingBuffer(RingBuffer const&) = delete;
      RingBuffer& operator=(RingBuffer const&) = delete;

      ~RingBuffer()
      {
	UnmapViewOfFile(send_ring_);
	UnmapViewOfFile(receive_ring_);
      }

      HANDLE send_ring_tail_moved()
      {
	return send_ring_tail_moved_();
      }

      HANDLE receive_ring_tail_moved()
      {
	return receive_ring_tail_moved_();
      }

      TUN_RING* send_ring()
      {
	return send_ring_;
      }

      TUN_RING* receive_ring()
      {
	return receive_ring_;
      }

      AsioEvent& send_tail_moved_asio_event()
      {
	return send_tail_moved_asio_event_;
      }

#ifdef HAVE_JSON
      void serialize(Json::Value& json)
      {
	json["send_ring_hmem"] = BufHex::render(send_ring_hmem());
	json["receive_ring_hmem"] = BufHex::render(receive_ring_hmem());
	json["send_ring_tail_moved"] = BufHex::render(send_ring_tail_moved());
	json["receive_ring_tail_moved"] = BufHex::render(receive_ring_tail_moved());
      }
#endif

    protected:
      Win::ScopedHANDLE send_ring_hmem;
      Win::ScopedHANDLE receive_ring_hmem;
      Win::Event send_ring_tail_moved_{FALSE};
      Win::Event receive_ring_tail_moved_{FALSE};
      AsioEvent send_tail_moved_asio_event_;

      TUN_RING* send_ring_ = nullptr;
      TUN_RING* receive_ring_ = nullptr;
    };
  }
}
