/* ----------------------------------------------------------------------------
  Copyright (c) 2021, Daan Leijen
  This is free software; you can redistribute it and/or modify it
  under the terms of the MIT License. A copy of the license can be
  found in the "LICENSE" file at the root of this distribution.
-----------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>  
#include <sys/stat.h>

#include "../include/isocline.h"
#include "common.h"
#include "history.h"
#include "stringbuf.h"

#define IC_MAX_HISTORY (200)

struct history_s {
  ssize_t  count;              // current number of entries in use
  ssize_t  len;                // size of elems 
  const char** elems;         // history items (up to count)
  const char*  fname;         // history file
  alloc_t* mem;
  bool     allow_duplicates;   // allow duplicate entries?
};

ic_private history_t* history_new(alloc_t* mem) {
  history_t* h = mem_zalloc_tp(mem,history_t);
  h->mem = mem;
  return h;
}

ic_private void history_free(history_t* h) {
  if (h == NULL) return;
  history_clear(h);
  if (h->len > 0) {
    mem_free( h->mem, h->elems );
    h->elems = NULL;
    h->len = 0;
  }
  mem_free(h->mem, h->fname);
  h->fname = NULL;
  mem_free(h->mem, h); // free ourselves
}

ic_private bool history_enable_duplicates( history_t* h, bool enable ) {
  bool prev = h->allow_duplicates;
  h->allow_duplicates = enable;
  return prev;
}

ic_private ssize_t  history_count(const history_t* h) {
  return h->count;
}

//-------------------------------------------------------------
// push/clear
//-------------------------------------------------------------

ic_private bool history_update( history_t* h, const char* entry ) {
  if (entry==NULL) return false;
  history_remove_last(h);
  history_push(h,entry);
  //debug_msg("history: update: with %s; now at %s\n", entry, history_get(h,0));
  return true;
}

static void history_delete_at( history_t* h, ssize_t idx ) {
  if (idx < 0 || idx >= h->count) return;
  mem_free(h->mem, h->elems[idx]);
  for(ssize_t i = idx+1; i < h->count; i++) {
    h->elems[i-1] = h->elems[i];
  }
  h->count--;
}

ic_private bool history_push( history_t* h, const char* entry ) {
  if (h->len <= 0 || entry==NULL)  return false;
  // remove any older duplicate
  if (!h->allow_duplicates) {
    for( int i = 0; i < h->count; i++) {
      if (strcmp(h->elems[i],entry) == 0) {
        history_delete_at(h,i);
      }
    }
  }
  // insert at front
  if (h->count == h->len) {
    // delete oldest entry
    history_delete_at(h,0);    
  }
  assert(h->count < h->len);
  h->elems[h->count] = mem_strdup(h->mem,entry);
  h->count++;
  return true;
}


static void history_remove_last_n( history_t* h, ssize_t n ) {
  if (n <= 0) return;
  if (n > h->count) n = h->count;
  for( ssize_t i = h->count - n; i < h->count; i++) {
    mem_free( h->mem, h->elems[i] );
  }
  h->count -= n;
  assert(h->count >= 0);    
}

ic_private void history_remove_last(history_t* h) {
  history_remove_last_n(h,1);
}

ic_private void history_clear(history_t* h) {
  history_remove_last_n( h, h->count );
}

ic_private const char* history_get( const history_t* h, ssize_t n ) {
  if (n < 0 || n >= h->count) return NULL;
  return h->elems[h->count - n - 1];
}

ic_private bool history_search( const history_t* h, ssize_t from /*including*/, const char* search, bool backward, ssize_t* hidx, ssize_t* hpos ) {
  const char* p = NULL;
  ssize_t i;
  if (backward) {
    for( i = from; i < h->count; i++ ) {
      p = strstr( history_get(h,i), search);
      if (p != NULL) break;
    }
  }
  else {
    for( i = from; i >= 0; i-- ) {
      p = strstr( history_get(h,i), search);
      if (p != NULL) break;
    }
  }
  if (p == NULL) return false;
  if (hidx != NULL) *hidx = i;
  if (hpos != NULL) *hpos = (p - history_get(h,i));
  return true;
}

//-------------------------------------------------------------
// 
//-------------------------------------------------------------

ic_private void history_load_from(history_t* h, const char* fname, long max_entries ) {
  history_clear(h);
  h->fname = mem_strdup(h->mem,fname);
  if (max_entries == 0) {
    assert(h->elems == NULL);
    return;
  }
  if (max_entries < 0 || max_entries > IC_MAX_HISTORY) max_entries = IC_MAX_HISTORY;
  h->elems = (const char**)mem_zalloc_tp_n(h->mem, char*, max_entries );
  if (h->elems == NULL) return;
  h->len = max_entries;
  history_load(h);
}




//-------------------------------------------------------------
// save/load history to file
//-------------------------------------------------------------

static char from_xdigit( int c ) {
  if (c >= '0' && c <= '9') return (char)(c - '0');
  if (c >= 'A' && c <= 'F') return (char)(10 + (c - 'A'));
  if (c >= 'a' && c <= 'f') return (char)(10 + (c - 'a'));
  return 0;
}

static char to_xdigit( uint8_t c ) {
  if (c <= 9) return ((char)c + '0');
  if (c >= 10 && c <= 15) return ((char)c - 10 + 'A');
  return '0';
}

static bool ic_isxdigit( int c ) {
  return ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || (c >= '0' && c <= '9'));
}

static bool history_read_entry( history_t* h, FILE* f, stringbuf_t* sbuf ) {
  sbuf_clear(sbuf);
  while( !feof(f)) {
    int c = fgetc(f);
    if (c == EOF || c == '\n') break;
    if (c == '\\') {
      c = fgetc(f);
      if (c == 'n')       { sbuf_append(sbuf,"\n"); }
      else if (c == 'r')  { /* ignore */ }  // sbuf_append(sbuf,"\r");
      else if (c == 't')  { sbuf_append(sbuf,"\t"); }
      else if (c == '\\') { sbuf_append(sbuf,"\\"); }
      else if (c == 'x') {
        int c1 = fgetc(f);         
        int c2 = fgetc(f);
        if (ic_isxdigit(c1) && ic_isxdigit(c2)) {
          char chr = from_xdigit(c1)*16 + from_xdigit(c2);
          sbuf_append_char(sbuf,chr);
        }
        else return false;
      }
      else return false;
    }
    else sbuf_append_char(sbuf,(char)c);
  }
  if (sbuf_len(sbuf)==0 || sbuf_string(sbuf)[0] == '#') return true;
  return history_push(h, sbuf_string(sbuf));
}

static bool history_write_entry( const char* entry, FILE* f, stringbuf_t* sbuf ) {
  sbuf_clear(sbuf);
  //debug_msg("history: write: %s\n", entry);
  while( entry != NULL && *entry != 0 ) {
    char c = *entry++;
    if (c == '\\')      { sbuf_append(sbuf,"\\\\"); }
    else if (c == '\n') { sbuf_append(sbuf,"\\n"); }
    else if (c == '\r') { /* ignore */ } // sbuf_append(sbuf,"\\r"); }
    else if (c == '\t') { sbuf_append(sbuf,"\\t"); }
    else if (c < ' ' || c > '~' || c == '#') {
      char c1 = to_xdigit( (uint8_t)c / 16 );
      char c2 = to_xdigit( (uint8_t)c % 16 );
      sbuf_append(sbuf,"\\x"); 
      sbuf_append_char(sbuf,c1); 
      sbuf_append_char(sbuf,c2);            
    }
    else sbuf_append_char(sbuf,c);
  }
  //debug_msg("history: write buf: %s\n", sbuf_string(sbuf));
  
  if (sbuf_len(sbuf) > 0) {
    sbuf_append(sbuf,"\n");
    fputs(sbuf_string(sbuf),f);
  }
  return true;
}

ic_private void history_load( history_t* h ) {
  if (h->fname == NULL) return;
  FILE* f = fopen(h->fname, "r");
  if (f == NULL) return;
  stringbuf_t* sbuf = sbuf_new(h->mem);
  if (sbuf != NULL) {
    while (!feof(f)) {
      if (!history_read_entry(h,f,sbuf)) break; // error
    }
    sbuf_free(sbuf);
  }
  fclose(f);
}

ic_private void history_save( const history_t* h ) {
  if (h->fname == NULL) return;
  FILE* f = fopen(h->fname, "w");
  if (f == NULL) return;
  #ifndef _WIN32
  chmod(h->fname,S_IRUSR|S_IWUSR);
  #endif
  stringbuf_t* sbuf = sbuf_new(h->mem);
  if (sbuf != NULL) {
    for( int i = 0; i < h->count; i++ )  {
      if (!history_write_entry(h->elems[i],f,sbuf)) break;  // error
    }
    sbuf_free(sbuf);
  }
  fclose(f);  
}