#pragma once
#include <sstream>

static std::wstring dirnameOf(const std::wstring& p){
  auto pos = p.find_last_of(L"\\/"); return (pos==std::wstring::npos)? L"." : p.substr(0,pos);
}
static std::string lastErrorString(DWORD e){
  LPWSTR msg=nullptr;
  FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
    nullptr,e,0,(LPWSTR)&msg,0,nullptr);
  std::wstring w = msg? std::wstring(msg) : L"(unknown)";
  if(msg) LocalFree(msg);
  std::string s(w.begin(), w.end());
  // strip CRLF
  s.erase(remove(s.begin(), s.end(), '\r'), s.end());
  s.erase(remove(s.begin(), s.end(), '\n'), s.end());
  std::ostringstream o; o<<"WinErr "<<e<<": "<<s; return o.str();
}

bool loadWithPolicy(const std::wstring& path, std::string& err, HMODULE& out) {
  out = nullptr;
  // Ensure safe search policy (no current directory)
  SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_USER_DIRS);
  auto dir = dirnameOf(path);
  auto cookie = AddDllDirectory(dir.c_str()); // extend search to DLL’s directory

  out = LoadLibraryExW(path.c_str(), nullptr,
    LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
  if(!out){
    DWORD ec = GetLastError();
    err = "LoadLibraryEx failed for \"" + std::string(path.begin(), path.end()) + "\"; " + lastErrorString(ec);
  }
  if(cookie) RemoveDllDirectory(cookie);
  return out != nullptr;
}

struct ECI {
  using HANDLE_T = int;
  HMODULE mod = nullptr;
  ...
  bool load(const std::wstring& path, std::string& err) {
    if(!loadWithPolicy(path, err, mod)) return false;
#define RESOLVE(name) name = (decltype(name))GetProcAddress(mod, #name); if(!name){ err = "GetProcAddress failed for " #name; return false; }
    RESOLVE(eciNewEx); RESOLVE(eciDelete); RESOLVE(eciSetOutputBuffer);
    RESOLVE(eciRegisterCallback); RESOLVE(eciAddText); RESOLVE(eciInsertIndex);
    RESOLVE(eciSynthesize); RESOLVE(eciStop); RESOLVE(eciSetParam);
    RESOLVE(eciSetVoiceParam); RESOLVE(eciGetVoiceParam); RESOLVE(eciCopyVoice);
#undef RESOLVE
    return true;
  }
  ~ECI(){ if(mod) FreeLibrary(mod); }
};
#include <windows.h>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <functional>

// Minimal ECI API surface (dynamic-load)
struct ECI {
  using HANDLE_T = int; // ECI handle is int (per ECI headers)
  HMODULE mod = nullptr;

  // Function pointers
  HANDLE_T (WINAPI *eciNewEx)(unsigned long);
  int (WINAPI *eciDelete)(HANDLE_T);
  int (WINAPI *eciSetOutputBuffer)(HANDLE_T, int, short*);
  using CB = int (WINAPI*)(HANDLE_T,int,int,int,void*);
  int (WINAPI *eciRegisterCallback)(HANDLE_T, CB, void*);
  int (WINAPI *eciAddText)(HANDLE_T, const char*);
  int (WINAPI *eciInsertIndex)(HANDLE_T, int);
  int (WINAPI *eciSynthesize)(HANDLE_T);
  int (WINAPI *eciStop)(HANDLE_T);
  int (WINAPI *eciSetParam)(HANDLE_T, int, int);
  int (WINAPI *eciSetVoiceParam)(HANDLE_T, int, int, int);
  int (WINAPI *eciGetVoiceParam)(HANDLE_T, int, int);
  int (WINAPI *eciCopyVoice)(HANDLE_T, int, int);

  bool load(const std::wstring& path) {
    mod = LoadLibraryW(path.c_str());
    if (!mod) return false;
#define RESOLVE(name) name = (decltype(name))GetProcAddress(mod, #name); if(!name) return false;
    RESOLVE(eciNewEx); RESOLVE(eciDelete); RESOLVE(eciSetOutputBuffer);
    RESOLVE(eciRegisterCallback); RESOLVE(eciAddText); RESOLVE(eciInsertIndex);
    RESOLVE(eciSynthesize); RESOLVE(eciStop); RESOLVE(eciSetParam);
    RESOLVE(eciSetVoiceParam); RESOLVE(eciGetVoiceParam); RESOLVE(eciCopyVoice);
#undef RESOLVE
    return true;
  }
  ~ECI(){ if(mod) FreeLibrary(mod); }
};

class EciEngine {
public:
  static constexpr int kSampleRate = 11025;
  static constexpr int kChannels = 1;
  static constexpr int kBits = 16;
  static constexpr int kChunkSamples = 3300; // about 300ms

  using AudioSink = std::function<void(const char* data, size_t n)>; // emits raw PCM bytes
  using IndexSink = std::function<void(int)>; // >=0 for marker; -1 for end

  bool init(const std::wstring& dllPath, unsigned long voiceLangId, std::string& err);
  void setSinks(AudioSink a, IndexSink i);

  void addText(const std::string& mbcs);
  void insertIndex(int idx);
  void synthesize();
  void stop();
  void setParam(int p, int v);
  void setVParam(int p, int v, bool temporary);
  int  getVParam(int p);
  void copyVoice(int variant);

  std::unordered_map<unsigned,int> currentVParams();

private:
  static int WINAPI cbThunk(int h, int msg, int lp, int dt, void* user);
  int onCallback(int msg, int lp);

  ECI eci_{};
  ECI::HANDLE_T h_ = 0;
  std::vector<short> buf_{};
  std::string stash_{}; // byte stash for emitting whole chunks
  std::mutex mu_{};
  AudioSink audio_{};
  IndexSink index_{};
  bool speaking_ = false;
};