#include <textmenu.h>
#include <function.h>

#include <stdio.h>
#include <dos.h>

#define GO '$'
 // Служебный символ Str-файла
#define MAX_GO 9
 // Максимальное число ссылок на другие статьи в тексте одной статьи
#define MAX_LEN 8000
 // Длина строки-буфера для статьи
#define MAX_STR 128
 // Максимальное количество строк в статье
#define HISTORY_LEN 100
 //Длина буфера для записи истории
#define MAX_NAME_LEN 128
 //Размер буферной строки для чтения имени str-файла и строк этого файла

unsigned HisPtr=0,History[HISTORY_LEN]; //Буфер для истории игры
FILE *fp;                 //STR-файл
long Size;                //Размер STR-файла
unsigned ErrorNum=0;      //Номер ошибки
MODE KolNotes=0;          //Число ссылок из текущей статьи
unsigned Notes[MAX_GO+1]; //Ссылки из текущей статьи
unsigned     MaxTag;      //Число статей в истории
MODE String[MAX_LEN+1];   //Строка-буфер под статью истории

static MODE Name[MAX_NAME_LEN];
 //Строка для чтения имени str-файла и его строк
#define CONFIG_NAME "story.dat"
FILE *fc;                    //Файл STORY.DAT
unsigned OldNote,SelectNote; //Предыдущая и текущая статья
KEY SYM=0;                   //Нажатая клавиша

MODE GetString (MODE *Str) {
 MODE *Ptr;
 Ptr=fgets (Str,MAX_NAME_LEN,fp);
 if (Ptr==NULL) return 0;
 return 1;
}

#define TEXTCOLOR YELLOW
#define SELECTCOLOR WHITE
#define BACKCOLOR BLUE
 // Оформление окна помощи

void Help (MODE Index) {
 MODE *Mes[]={
  "Просто выберите пункт назначения!\0",  //0..9
  "Сейчас будет создан файл STORY.DAT\n"  //11
  "с информацией о Вашей истории...\0",
  "   Используйте клавиши со стрелками для\n" //12
  "листания текста; Клавишей ESC можно от-\n"
  "казаться от игры, Ваша позиция при этом\n"
  "будет  сохранена. Любая  другая клавиша\n"
  "вызывает меню выбора пункта назначения.\0"
 };
 EXPLODE=YES; SHADOW=YES;
 save (9,19,16,61);  Fun_Bar(" Нажмите любую клавишу...");
 window (9,19,15,59);
 if (Index<10) Index=0;
 else Index-=10;
 Print (10,20,TEXTCOLOR,SELECTCOLOR,Mes[Index]);
 AnyClick();  
 Fun_Bar(""); Clear_Buf();
 restore (9,19,16,61);
}

MODE VoidString (MODE *String) {
 MODE *Ptr=String;
 while (*Ptr==' ' || *Ptr=='\t') Ptr++;
 if (*Ptr=='\r' || *Ptr=='\n') return 1;
 return 0;
}

unsigned GetText (MODE *String) {
 //Возвращает длину текста
 MODE c; unsigned Pos;
 String[0]='\0'; Pos=0;
 while (1) {
  c=GetString (&String[Pos]);
  if (!c || VoidString (&String[Pos]) || Pos>MAX_LEN-MAX_NAME_LEN) {
   String[Pos]='\0';   return Pos;
  }
  Pos+=strlen_ (&String[Pos]);
 }
}

MODE IsHeader (MODE *Str) {
 MODE *Ptr=Str;
 while (*Ptr==' ' || *Ptr=='\t') Ptr++;
 if (*Ptr==GO) {
  Ptr++;
  while (isdigit(*Ptr)) Ptr++;
  if (*Ptr!=GO) return 1;
 }
 return 0;
}

void FindTags (MODE *String,MODE *KolNotes,unsigned *Notes) {
 MODE *Ptr,Len;
 (*KolNotes)=0;
 while (*String) {
  if (*String==GO) {
   String++; Ptr=String; Len=0;
   while (*String) {
    if (!isdigit(*String)) {
     if (*String==GO && Len) {
      Push (String);
      Notes[(*KolNotes)]=atoi_ (Ptr);
      Pop (String);
      (*KolNotes)++;
      if (*KolNotes==MAX_GO) return;
      String++;
     }
     else { String++; break; }
    }
    else {
     Len++; String++;
    }
   }
  }
  else String++;
 }
 return;
}

MODE ColStr (MODE *String, KEY *Met) {
 KEY Len=0,Kol;
 *Met=0; Met++; Kol=1;
 while (*String) {
  if (*String=='\n') {
   *Met=Len+1; Met++; Kol++;
   if (Kol==MAX_STR) goto OUT;
  }
  String++; Len++;
 }
OUT: return Kol;
}

void WaitRelease(void) {
 buttons=0x00;
 while(!ms_release());
}

MODE Click(void) {
 if (Mouse) {
  ms_state();
  if (buttons) { WaitRelease(); return YES; }
 }
 return NO;
}

KEY DrawText (unsigned Num, MODE *String) {
 MODE Ris=NO,Str=0,AllStr,i;
 MODE *Hlp=" $ESC$ Отказ  $F1$ Помощь  $\030$ Листать вверх  $\031$ Листать вниз  $Др.клавиша$ Выбор",
  *Shape="-12345-",
  Pos_Keys[]={0,11,22,39,55,72};
 KEY c,Bar_Keys[]={ESC,F1,UP,DOWN,ENTER},
  Met[MAX_STR]; //Met - метки начала строк в String (для листания)
 AllStr=ColStr  (String, Met);
 EXPLODE=YES; SHADOW=YES;
 window (1,1,22,77);
 itoa_(Num,&Shape[1]);
 c=strlen_(Shape); Shape[c]='-'; Shape[c+1]='\0';
 center_string (1,2,76,Shape);
AGAIN:
 clear (2,2,21,76);
 //Полоса бегунка:
 CURSOR_OFF();
 write_char (2, 77,'\036');
 write_char (21,77,'\037');
 for (i=3; i<21; i++) write_char (i,77,'▒');
 i=3+Str*18/AllStr;
 // if (i<2) i=2; else if (i>21) i=21;
 write_char (i,77,'\n');
 CURSOR_ON();

 EXPLODE=NO;
 Print (2,2,TEXTCOLOR,SELECTCOLOR,&String[Met[Str]]);
 Fun_Bar (Hlp);
 while (1) {
  if (Mouse) while (!Hit_Key()) {
   ms_state();
   if (buttons==0x01) {
    if (Fun_Bar_Keys(5,Bar_Keys,Pos_Keys)) goto KEYBOARD;
    if (MouseY==77) {
     if (MouseX== 2) { Set_Key (1,UP); /*WaitRelease();*/ break; }
     if (MouseX==21) { Set_Key (1,DOWN); /*WaitRelease();*/ break; }
    }
   }
  }
KEYBOARD:
  c=Get_Key();
  switch (c) {
   case UP: case LEFT:
    if (Str>0) { Str--;  Ris=YES; } break;
   case DOWN:  case RIGHT:
    if (Str<AllStr-2) { Str++; Ris=YES; } break;
   case PAGE_UP: case HOME:
    if (Str>0) { Str=0;  Ris=YES; }
   break;
   case PAGE_DO: case END:
    if (Str<AllStr-2) { Str=AllStr-2; Ris=YES; }
   break;
   case F1:
    Help (12);
    Fun_Bar (Hlp);
   break;
   default: goto OUT;
  }
  if (Ris) {   Ris=NO;   goto AGAIN;  }
 }
OUT: return c;
}

unsigned GetNumber(MODE *Str) {
 while (*Str!=GO) Str++;
 return (atoi_(Str+1));
}

static MODE Select; //Выбранный вариант меню пунктов назначения
void Point0 (void) { Select=0; Set_Key (1,ESC); }
void Point1 (void) { Select=1; Set_Key (1,ESC); }
void Point2 (void) { Select=2; Set_Key (1,ESC); }
void Point3 (void) { Select=3; Set_Key (1,ESC); }
void Point4 (void) { Select=4; Set_Key (1,ESC); }
void Point5 (void) { Select=5; Set_Key (1,ESC); }
void Point6 (void) { Select=6; Set_Key (1,ESC); }
void Point7 (void) { Select=7; Set_Key (1,ESC); }
void Point8 (void) { Select=8; Set_Key (1,ESC); }
void Point9 (void) { Select=9; Set_Key (1,ESC); }

unsigned NotesMenu (MODE Kol,unsigned *Sel) {
 MENU Menu={ 9,33, 16,47, 5, "dDвВ1!1!2@2\"3#3:4$4$5%5%6^6,7&7.8*8?9(9( ",
  WHITE, BLACK, RED, 0, YES, 0};
 MENUITEM Item[]={
  {"$В$ернуться   ",YES,Point0},
  {"$1$.Пункт     ",YES,Point1},
  {"$2$.Пункт     ",YES,Point2},
  {"$3$.Пункт     ",YES,Point3},
  {"$4$.Пункт     ",YES,Point4},
  {"$5$.Пункт     ",YES,Point5},
  {"$6$.Пункт     ",YES,Point6},
  {"$7$.Пункт     ",YES,Point7},
  {"$8$.Пункт     ",YES,Point8},
  {"$9$.Пункт     ",YES,Point9}
 };
 MODE i, *Ptr;
 Menu.kol=Kol;
 Ptr=&Menu.keys[(Kol<<2)];
 Push (Ptr);
 Menu.keys[(Kol<<2)]='\0';
 Menu.x2=10+Kol;
 for (i=1; i<Kol; i++) {
  itoa_ (Sel[i],&Item[i].str[10]);
 }
 Popup (&Menu,Item);
 Pop (Ptr);
 Fun_Bar("");
 return (Sel[Select]);
}

void Error (MODE Num) {
 MODE *Mes[]={
"Требуется запускать данную программу с\n"    //0
"параметром - именем Story-файла;\n"
"Story-файл содержит статьи, пронумеро-\n"
"ванные на отдельной строке и разделен-\n"
"ные пустыми строками. Для  обозначения\n"
"номера статьи используется символ '$$',\n"
"для ссылок  на другие статьи  их номер\n"
"заключается в этот же символ. Пример:\n"
" $$1\n"
" Статья номер 1. В ней есть ссылка на\n"
"статью номер $$2$$.\n"
" $Со второго запуска параметр не нужен.$\0",
"Не могу открыть файл, указанный  в ка-\n"    //1
"честве параметра!  Для получения помо-\n"
"щи запускайте программу без параметров\n"
"или читайте документацию ($STORY.DOC$).\0",
"Не могу открыть файл для записи! Долж-\n"   //2
"но быть хотя бы $50$ Кбайт дискового про-\n"
"странства!\0",
"Не могу открыть файл, описанный в кон-\n"  //3
"фигурации, для продолжения игры! Чтобы\n"
"начать новую игру,сотрите с диска файл\n"
"$STORY.DAT$ и запустите  программу с Str\n"
"-файлом в качестве параметра.  Для по-\n"
"лучения помощи сотрите $STORY.DAT$ и за-\n"
"пустите программу  без параметров.\0",
" Не могу открыть созданный  файл\n"        //4
" STORY.DAT! Возможно, ошибка ра-\n"
" боты с диском!\0",
"Ссылка не найдена в тексте: _____\0",       //5
"Недопустимый номер ссылки:  _____\0", //6
"Больше 9 ссылок из статьи:  _____\0" //7
};
 EXPLODE=YES; SHADOW=YES;
 window (5,20,19,59);
 if (Num>4) {
  itoa_ (ErrorNum,&Mes[Num][28]);
 }
 Print (7,21,TEXTCOLOR,SELECTCOLOR,Mes[Num]);
 AnyClick();
}

long GetOffset (unsigned x) { //Смещение до записи номер x
 return (x*25+MAX_NAME_LEN+10);
}

void  ReSize (unsigned New) {
 //Меняет размер с Old на New записей
 long OldSize,NewSize,l;
 MODE Nul=0;
 fseek (fc,0,SEEK_END);
 fgetpos (fc,&OldSize);
 NewSize=GetOffset(New+1);
 if (OldSize<NewSize) {
  for (l=OldSize; l<NewSize; l++) fwrite (&Nul,1,1,fc);
 }
}


MODE FindNote (unsigned Num,MODE *String) {
 // 0 -успешно, 5-Ссылка не найдена, 7-Много ссылок
 long Off; unsigned Len;
 if (Num>MaxTag) return 5; //Ссылка не найдена
 fseek (fc, GetOffset (Num), SEEK_SET);
 fread (&Off,4,1,fc);
 fread (&Len,2,1,fc);
 if (Len>MAX_LEN) Len=MAX_LEN;
 fread (&KolNotes,1,1,fc);
 if (KolNotes>MAX_GO) return 7; //Много ссылок
 fread (&Notes[1],2,9,fc);
 fseek (fp, Off, SEEK_SET);
 fread (String,Len,1,fp);
 String[Len]='\0';
 SYM=DrawText (Num,String);
 return 0;
}

unsigned MakeConfig (void) {
 //Возвращает максимальный номер прочитанной статьи
 MODE c; long Len=0,Off;
 unsigned Null=0,CurNum;
 Help(11);
 fseek (fc,0,SEEK_SET);
 fwrite (Name,MAX_NAME_LEN,1,fc); //Имя файла с текстом
 fseek (fp,0,SEEK_END);
 fgetpos (fp,&Len);
 fwrite (&Len,4,1,fc);       //Размер файла с текстом
 for (c=0; c<4; c++) fwrite (&Null,2,1,fc);
 fseek (fp,0,SEEK_SET);
 MaxTag=0;

 while (1) {
  c=GetString (Name);    //FileStr
  if (!c) break;
  if (IsHeader (Name)) {   //FileStr
   CurNum=GetNumber(Name); //FileStr
   fgetpos (fp, &Off);
   Len=GetText (String);               //Строка
   FindTags (String,&KolNotes,Notes);  //Массив ссылок
   if (CurNum>MaxTag) {
    ReSize (CurNum); MaxTag=CurNum;
   }
   fseek (fc,GetOffset(CurNum),SEEK_SET);
   fwrite (&Off,4,1,fc);      fwrite (&Len,2,1,fc);
   fwrite (&KolNotes,1,1,fc);
   for (c=0; c<KolNotes; c++) fwrite (&Notes[c],2,1,fc);
  }
 }
 fseek (fc,MAX_NAME_LEN+4,SEEK_SET);
 fwrite (&MaxTag,2,1,fc);
 return MaxTag;
}

void ReadConfig (void) {
 fseek (fc,0,SEEK_SET);
 fread (Name,MAX_NAME_LEN,1,fc); //Имя файла с текстом
 fread (&Size,4,1,fc);       //Размер файла с текстом
 fread (&MaxTag,2,1,fc);     //Число статей
 fread (&OldNote,2,1,fc); //Предыдущая статья
 fread (&SelectNote,2,1,fc); //Текущая статья
}
///////////////////Обработка прерывания времени - только для DEMO
#define KEY_PAUSE 4
 //Период смены экрана для Демо
void interrupt (* old_int1C)(...); //Старый обработчик прерывания таймера
MODE  Count=0,Interval=0;  //Подсчет тактов таймера и секунд

void interrupt ISR_int1C(...) {
 old_int1C();
 disable();
 outportb(0x20, 0x20);
 enable();
 Count++;
 if (Count==18) {
  Count=0;
  Interval++;
  if (Interval==KEY_PAUSE+1) {
   Interval=0;
  }
 }
}

void Init_int1C(void) {
 old_int1C = getvect(0x1C);
 disable();
 setvect(0x1C, ISR_int1C);
 enable();
 return;
}

MODE Shut_int1C (void) {
 disable();
 if(getvect(0x1C) != ISR_int1C) { enable(); return NO; }
 setvect(0x1C, old_int1C);
 enable();
 return YES;
}

///////////////////

void main (int argc,char **argv) {
 unsigned CurNote,DemoCount;
 MODE c,First=NO; long CurSize;

 if (!textmode()) return;
 ATTR=0x70;
 clear (0,0,24,79);
 WIN_X2=21; WIN_Y2=76;
 Mouse=Init_Mouse();

 if (argc>1)  //Если есть параметр, он имеет приоритет
  goto OPEN_NEW;
 fc=fopen (CONFIG_NAME,"r+b"); //Открываем файл конфигурации
 if (fc==NULL) {
OPEN_NEW:
  if (argc!=2) { //Если не найден файл конфигурации, д.б. передан параметр
   Error (0); goto RET2;
  }
  fp=fopen  (argv[1],"rt"); //Открываем файл-параметр
  if (fp==NULL) {
   Error (1); goto RET2;
  }
  strcpy_ (Name,argv[1]);
  fc=fopen (CONFIG_NAME,"wb"); //Пытаемся создать новый
  if (fc==NULL) { Error (2); goto RET2; }
  SelectNote=0; OldNote=0;
  Size=-1L;
 }
 else {
  ReadConfig(); //Прочитать конфигурацию
  fp=fopen  (Name,"rt"); //Открываем текст
  if (fp==NULL) {
   Error (3); goto RET2;
  }
 }
 fseek (fp,0,SEEK_END);
 fgetpos (fp,&CurSize); //Сравниваем размер текста с сохраненным
 if (CurSize!=Size) {
  MaxTag=MakeConfig(); //Создаем файл конфигурации
  fclose (fc);
  fc=fopen (CONFIG_NAME,"r+b");
  Clear_Buf();
  if (fc==NULL) {
   Error (4); goto RET2;
  }
  ReadConfig(); //Прочитать конфигурацию
  First=YES; //Первая игра!
 }

 show_cursor (0);

 Init_int1C();

DEMO_AGAIN:
 ATTR=0x1e; window (8,7,14,70);
 Print (10,9,YELLOW,YELLOW,
  "█▀▀▀ ▀▀█▀▀ █▀▀█ █▀▀█ █   █    ▀▀█▀▀ █▀▀▀ █    █    █▀▀▀ █▀▀█\n"
  "▀▀▀█   █   █  █ █▀█▀  ▀█▀       █   █▀   █    █    █▀   █▀█▀\n"
  "▀▀▀▀   ▀   ▀▀▀▀ ▀ ▀▀   ▀        ▀   ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀ ▀▀\0");
 if (!First) {
  CurSize=GetOffset(MaxTag+1);
  DemoCount=0;
  fseek (fc, CurSize, SEEK_SET);
 }
 while (Interval!=KEY_PAUSE) {
  if (Hit_Key() || Click()) {
OUT_DEMO:
   First=YES; Clear_Buf(); break;
  }
 }
 while (1) {
  ATTR=set_attr (TEXTCOLOR,BACKCOLOR);
  if (!First) while (1) { //Демо
   if (Hit_Key() || Click()) goto OUT_DEMO;
   if (Interval==KEY_PAUSE) { //Очередь пуста
    fseek (fc, CurSize+(DemoCount<<1), SEEK_SET); //След.шаг истории
    DemoCount++;
    c=fread (&CurNote,2,1,fc);
    Interval=0;
    if (!c) goto DEMO_AGAIN; //Конец Демо - начать его сначала
    Set_Key (1,ENTER);
    c=FindNote (CurNote,String);
    if (c) goto OUT_DEMO; //Ошибка в Демо - прекратить его
    Fun_Bar (" Нажмите любую клавишу для выхода из Демо...");
   }
  }
  if (SelectNote>MaxTag) {
   c=6; goto ERROR; // 6 - Недопустимый номер ссылки
  }
  c=FindNote (SelectNote,String);
  if (c) {
ERROR:
   ErrorNum=SelectNote;
   SelectNote=OldNote;
   Error (c);  // 5 - Ошибка в игре - ссылка не найдена
   goto RET;   // 7 - Много ссылок
  }
  if (SYM==ESC) break;
  Notes[0]=OldNote; KolNotes++;
  CurNote=NotesMenu (KolNotes,Notes);
  OldNote=SelectNote;
  SelectNote=CurNote;
  History[HisPtr]=SelectNote;
  HisPtr++;
  if (HisPtr==HISTORY_LEN) { //Запись истории
   fseek (fc,0,SEEK_END);
   for (c=0; c<HISTORY_LEN; c++) fwrite (&History[c],2,1,fc);
   HisPtr=0;
  }
 }
RET:
 fclose (fp);
 fseek (fc,MAX_NAME_LEN+6,SEEK_SET);
 fwrite (&OldNote,2,1,fc);
 fwrite (&SelectNote,2,1,fc);
 if (HisPtr) {
  fseek (fc,0,SEEK_END);
  for (c=0; c<HisPtr; c++) fwrite (&History[c],2,1,fc);
 }
 fclose (fc);
 Shut_int1C();
RET2:
 Close_System();
}
