#ifndef __LIBRESPOT_C_INTERNAL_H__
#define __LIBRESPOT_C_INTERNAL_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdbool.h>
#include <unistd.h>
#include <time.h>

#include <event2/event.h>
#include <event2/buffer.h>

#ifdef HAVE_ENDIAN_H
# include <endian.h>
#elif defined(HAVE_SYS_ENDIAN_H)
# include <sys/endian.h>
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
#include <libkern/OSByteOrder.h>
#define htobe16(x) OSSwapHostToBigInt16(x)
#define be16toh(x) OSSwapBigToHostInt16(x)
#define htobe32(x) OSSwapHostToBigInt32(x)
#define be32toh(x) OSSwapBigToHostInt32(x)
#define htobe64(x) OSSwapHostToBigInt64(x)
#define be64toh(x) OSSwapBigToHostInt64(x)
#endif

#define ARRAY_SIZE(x) ((unsigned int)(sizeof(x) / sizeof((x)[0])))

#include "librespot-c.h"
#include "crypto.h"
#include "http.h"

#include "proto/keyexchange.pb-c.h"
#include "proto/authentication.pb-c.h"
#include "proto/mercury.pb-c.h"
#include "proto/metadata.pb-c.h"
#include "proto/extended_metadata.pb-c.h"
#include "proto/clienttoken.pb-c.h"
#include "proto/login5.pb-c.h"
#include "proto/storage_resolve.pb-c.h"

// Disconnect from AP after this number of secs idle
#define SP_AP_DISCONNECT_SECS 60

// Max wait for AP to respond
#define SP_AP_TIMEOUT_SECS 10

// After a disconnect we try to reconnect, but if we are disconnected yet again
// we get the hint and won't try reconnecting again until after this cooldown
#define SP_AP_COOLDOWN_SECS 30

// How long after a connection failure we try to avoid an AP
#define SP_AP_AVOID_SECS 3600

// If client hasn't requested anything in particular
#define SP_BITRATE_DEFAULT SP_BITRATE_320

// A "mercury" response may contain multiple parts (e.g. multiple tracks), even
// though this implenentation currently expects just one.
#define SP_MERCURY_MAX_PARTS 32

// librespot uses /3, but -golang and -java use /4
#define SP_MERCURY_URI_TRACK "hm://metadata/4/track/"
#define SP_MERCURY_URI_EPISODE "hm://metadata/4/episode/"

// Special Spotify header that comes before the actual Ogg data
#define SP_OGG_HEADER_LEN 167

// For now we just always use channel 0, expand with more if needed
#define SP_DEFAULT_CHANNEL 0

// Download in chunks of 32768 bytes. The chunks shouldn't be too large because
// it makes seeking slow (seeking involves jumping around in the file), but
// large enough that the file can be probed from the first chunk.
// For comparison, Spotify for Windows seems to request 7300 byte chunks.
#define SP_CHUNK_LEN 32768

// Used to create default sysinfo, which should be librespot_[short sha]_[random 8 characters build id],
// ref https://github.com/plietar/librespot/pull/218. User may override, but
// as of 20220516 Spotify seems to have whitelisting of client name.
#define SP_CLIENT_NAME_DEFAULT "librespot"
#define SP_CLIENT_VERSION_DEFAULT "0.0.0"
#define SP_CLIENT_BUILD_ID_DEFAULT "aabbccdd"

// ClientIdHex from client_id.go. This seems to be the id that Spotify's own app
// uses. It is used in the call to https://clienttoken.spotify.com/v1/clienttoken.
// The endpoint doesn't accept client ID's of app registered at
// develop.spotify.com, so unfortunately spoofing is required.
#define SP_CLIENT_ID_DEFAULT "65b708073fc0480ea92a077233ca87bd"

// Shorthand for error handling
#define RETURN_ERROR(r, m) \
  do { ret = (r); sp_errmsg = (m); goto error; } while(0)

enum sp_error
{
  SP_OK_OTHER      = 3,
  SP_OK_WAIT       = 2,
  SP_OK_DATA       = 1,
  SP_OK_DONE       = 0,
  SP_ERR_OOM        = -1,
  SP_ERR_INVALID    = -2,
  SP_ERR_DECRYPTION = -3,
  SP_ERR_WRITE      = -4,
  SP_ERR_NOCONNECTION = -5,
  SP_ERR_OCCUPIED     = -6,
  SP_ERR_NOSESSION    = -7,
  SP_ERR_LOGINFAILED  = -8,
  SP_ERR_TIMEOUT      = -9,
};

enum sp_msg_type
{
  SP_MSG_TYPE_HTTP_REQ,
  SP_MSG_TYPE_HTTP_RES,
  SP_MSG_TYPE_TCP,
};

enum sp_seq_type
{
  SP_SEQ_STOP = 0,
  SP_SEQ_LOGIN,
  SP_SEQ_MEDIA_OPEN,
  SP_SEQ_MEDIA_GET,
  SP_SEQ_PONG,
};

enum sp_proto
{
  SP_PROTO_TCP,
  SP_PROTO_HTTP,
};

enum sp_media_type
{
  SP_MEDIA_UNKNOWN,
  SP_MEDIA_TRACK,
  SP_MEDIA_EPISODE,
};

enum sp_channel_state
{
  SP_CHANNEL_STATE_UNALLOCATED = 0,
  SP_CHANNEL_STATE_OPENED,
  SP_CHANNEL_STATE_PLAYING,
  SP_CHANNEL_STATE_PAUSED,
  SP_CHANNEL_STATE_STOPPED,
};

// From librespot-golang
enum sp_cmd_type
{
  CmdNone           = 0x00,
  CmdSecretBlock    = 0x02,
  CmdPing           = 0x04,
  CmdStreamChunk    = 0x08,

  CmdStreamChunkRes = 0x09,
  CmdChannelError   = 0x0a,
  CmdChannelAbort   = 0x0b,
  CmdRequestKey     = 0x0c,
  CmdAesKey         = 0x0d,
  CmdAesKeyError    = 0x0e,

  CmdImage          = 0x19,
  CmdCountryCode    = 0x1b,

  CmdPong           = 0x49,
  CmdPongAck        = 0x4a,
  CmdPause          = 0x4b,

  CmdProductInfo    = 0x50,
  CmdLegacyWelcome  = 0x69,

  CmdLicenseVersion = 0x76,
  CmdLogin          = 0xab,
  CmdAPWelcome      = 0xac,
  CmdAuthFailure    = 0xad,

  CmdMercuryReq     = 0xb2,
  CmdMercurySub     = 0xb3,
  CmdMercuryUnsub   = 0xb4,
};

struct sp_cmdargs
{
  struct sp_session *session;
  struct sp_credentials *credentials;
  struct sp_metadata *metadata;
  const char *username;
  const char *password;
  uint8_t *stored_cred;
  size_t stored_cred_len;
  const char *token;
  const char *path;
  int fd_read;
  int fd_write;
  size_t seek_pos;
  enum sp_bitrates bitrate;
  int use_legacy;

  sp_progress_cb progress_cb;
  void *cb_arg;
};

struct sp_conn_callbacks
{
  struct event_base *evbase;

  event_callback_fn response_cb;
  event_callback_fn timeout_cb;
};

struct sp_tcp_message
{
  enum sp_cmd_type cmd;

  bool encrypt;
  bool add_version_header;

  size_t len;
  uint8_t *data;
};

struct sp_message
{
  enum sp_msg_type type;

  union payload
    {
      struct sp_tcp_message tmsg;
      struct http_request hreq;
      struct http_response hres;
    } payload;
};

struct sp_server
{
  char address[256]; // e.g. ap-gue1.spotify.com
  unsigned short port; // normally 433 or 4070
  time_t last_connect_ts;
  time_t last_resolved_ts;
  time_t last_failed_ts;
};

struct sp_connection
{
  struct sp_server *server; // NULL or pointer to session.accesspoint

  bool is_connected;
  bool is_encrypted;

  // Where we receive data from Spotify
  int response_fd;
  struct event *response_ev;

  // Connection timers
  struct event *idle_ev;
  struct event *timeout_ev;

  // Holds incoming data
  struct evbuffer *incoming;

  // Buffer holding client hello and ap response, since they are needed for
  // MAC calculation
  bool handshake_completed;
  struct evbuffer *handshake_packets;

  struct crypto_keys keys;
  struct crypto_cipher encrypt;
  struct crypto_cipher decrypt;
};

struct sp_token
{
  char value[512]; // base64 string, actual size 360 bytes
  int32_t expires_after_seconds;
  int32_t refresh_after_seconds;
  time_t received_ts;
};

struct sp_mercury
{
  char *uri;
  char *method;
  char *content_type;

  uint64_t seq;

  uint16_t parts_num;
  struct sp_mercury_parts
    {
      uint8_t *data;
      size_t len;

      Track *track;
    } parts[SP_MERCURY_MAX_PARTS];
};

struct sp_file
{
  uint8_t id[20];

  char *path; // The Spotify URI, e.g. spotify:episode:3KRjRyqv5ou5SilNMYBR4E
  uint8_t media_id[16]; // Decoded value of the URIs base62
  enum sp_media_type media_type; // track or episode from URI

  // For files that are served via http/"new protocol" (we may receive multiple
  // urls).
  char *cdnurl[4];

  uint8_t key[16];

  uint16_t channel_id;

  // Length and download progress
  size_t len_bytes;
  size_t offset_bytes;
  size_t received_bytes;
  bool end_of_file;
  bool end_of_chunk;
  bool open;

  struct crypto_aes_cipher decrypt;
};

struct sp_channel_header
{
  uint16_t len;
  uint8_t id;
  uint8_t *data;
  size_t data_len;
};

struct sp_channel_body
{
  uint8_t *data;
  size_t data_len;
};

struct sp_channel
{
  int id;

  enum sp_channel_state state;

  bool is_data_mode;
  bool is_spotify_header_received;
  size_t seek_pos;
  size_t seek_align;

  // pipe where we write audio data
  int audio_fd[2];
  // Triggers when fd is writable
  struct event *audio_write_ev;
  // Storage of audio until it can be written to the pipe
  struct evbuffer *audio_buf;
  // How much we have written to the fd (only used for debug)
  size_t audio_written_len;

  struct sp_file file;

  // Latest header and body received
  struct sp_channel_header header;
  struct sp_channel_body body;

  // Callbacks made during playback
  sp_progress_cb progress_cb;
  void *cb_arg;
};

// Linked list of sessions
struct sp_session
{
  struct sp_server accesspoint;
  struct sp_server spclient;
  struct sp_server dealer;

  struct sp_connection conn;
  time_t cooldown_ts;

  // Use legacy protocol (non http, see seq_requests_legacy)
  bool use_legacy;

  struct http_session http_session;
  struct sp_token http_clienttoken;
  struct sp_token http_accesstoken;

  int n_hashcash_challenges;
  struct crypto_hashcash_challenge *hashcash_challenges;

  bool is_logged_in;
  struct sp_credentials credentials;
  char country[3]; // Incl null term

  enum sp_bitrates bitrate_preferred;

  struct sp_channel channels[8];

  // Points to the channel that is streaming, and via this information about
  // the current track is also available
  struct sp_channel *now_streaming_channel;

  // Current request in the sequence
  struct sp_seq_request *request;

  // Go to next step in a request sequence
  struct event *continue_ev;

  // Which sequence comes next
  enum sp_seq_type next_seq;

  struct sp_session *next;
};

struct sp_seq_request
{
  enum sp_seq_type seq_type;
  const char *name; // Name of request (for logging)
  enum sp_proto proto;
  int (*payload_make)(struct sp_message *, struct sp_session *);
  enum sp_error (*request_prepare)(struct sp_seq_request *, struct sp_conn_callbacks *, struct sp_session *);
  enum sp_error (*response_handler)(struct sp_message *, struct sp_session *);
};

struct sp_err_map
{
  int errorcode;
  const char *errmsg;
};


extern struct sp_callbacks sp_cb;
extern struct sp_sysinfo sp_sysinfo;
extern const char *sp_errmsg;

#endif // __LIBRESPOT_C_INTERNAL_H__
