// vi:ts=4:shiftwidth=4:expandtab
/***************************************************************************
                          zhcon.cpp  -  description
                             -------------------
    begin                : Fri Mar 23 2001
    copyright            : (C) 2001 by ejoy
    email                : ejoy@users.sourceforge.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <iostream>
#include <strstream>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>

#if defined(linux)
    #include <linux/limits.h>
    #include <linux/kd.h>
    #include <linux/vt.h>
    #include <pty.h>
#elif defined(__FreeBSD__)
    #include <termios.h>
    #include <machine/console.h>
    #include <libutil.h>
    #define TCSETA TIOCSETA
#endif

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <pwd.h>
#include <stdexcept>
#include <algorithm>
#include <locale.h>
#include "global.h"
#include "console.h"

#include "inputclient.h"
#include "inputmanager.h"
#include "nativeinputserver.h"
#ifdef HAVE_UNICON_LIB
    #include "uniconinputserver.h"
#endif

#include "gbkdecoder.h"
#include "gbdecoder.h"
#include "big5decoder.h"
#include "gb2big5decoder.h"
#include "big52gbdecoder.h"
#include "jisdecoder.h"
#include "kscmdecoder.h"
#include "configfile.h"
//#include "fade.h"
#include "zhcon.h"
#include "basefont.h"
//#include "popwin.h"
BaseFont* gpHzFont;
BaseFont* gpAscFont;

#ifndef NDEBUG
#include "debug.h"
ofstream debug("debug");
#endif

int gfd;
int Zhcon::mTtyPid = 0;
Zhcon::STATE Zhcon::mState = STOP;

char* Zhcon::mpTips[] = {
gettext_noop("Please visit zhcon.gnuchina.org for more information."),
gettext_noop("Zhcon supports GB2312,GBK,BIG5,JIS,KSCM encode."),
gettext_noop("You can use CTRL-ALT-0 to toggle prompt bar."),
gettext_noop("Press CTRL-ALT-H for online help."),
gettext_noop("Zhcon supports 12,14,16,24 pixel fonts,you can change in zhcon.conf"),
gettext_noop("You can use 24 pixel font on 17 monitor to get better visual effect."),
gettext_noop("Hit CTRL-D to quit zhcon."),
gettext_noop("You can find user manual in doc/."),
gettext_noop("Zhcon can use input methods from Unicon[TurboLinux],read manual for detail."),
gettext_noop("Zhcon is optimized for running under Linux FrameBuffer."),
gettext_noop("Press CTRL-F7 to change input style")
};

Zhcon::Zhcon()
:mpCon(NULL),
mpInputManager(NULL),
mpBar(NULL),
mShowTips(true)
{
    random_shuffle(mpTips,mpTips + sizeof(mpTips)/sizeof(char*) );
}

Zhcon::~Zhcon() {
    CleanUp();
}

void Zhcon::Init() {
    //reading config file
    string cfgfile = getenv("HOME");
    cfgfile += "/.zhconrc";
    if (access(cfgfile.c_str(), R_OK) != 0)
        cfgfile = "/etc/zhcon.conf";

//    char c;cin>>c;
    ConfigFile f(cfgfile.c_str());
    //the InitXXX sequence is important,do not change
    //unless you know what you are doing
    InitTty();
    // set blank line height, must before init font
    InitGraphDev(f);
    GraphMode();
    InitLocale(f); // include init font
    InitCon(f);
    InitMisc(f);
    InstallVtHandle();
    InstallSignal();
    ForkPty();
    InitInputManager(f);
    if (f.GetOption("startupmsg",true))
        StartupMsg();
}

void Zhcon::Run() {
    fd_set set;
    char buf[BUFSIZE];
    struct timeval tv;
    int n,r,t;
//    fadeout();
//    fadein();
    mState = RUNNING;
    while (mState == RUNNING) {
        t = 0;
        Encode e;
        do {
            mpCon->CursorBlink();
            FD_ZERO(&set);
            FD_SET(mConFd, &set);
            FD_SET(mTtyFd, &set);
            tv.tv_sec = 0;
            tv.tv_usec = 100000;                  /* 0.1 sec */
            r = select(FD_SETSIZE, &set, NULL, NULL, &tv);
            //auto detect buffer encode after idle for 1 sec
            if (++t == 10 && mAutoEncode != MANUAL) {
                e = mpCon->DetectBufferEncode();
                switch (e) {
                    case ASCII:
                        SetEncode(mDefaultEncode, mDefaultEncode);
                        break;
                    case GB2312:
                        SetEncode(GB2312,
                                  mAutoEncode ==
                                  AUTO_BIG5 ? BIG5 : GB2312); break;
                    case BIG5:
                        SetEncode(BIG5,
                                  mAutoEncode ==
                                  AUTO_GB ? GB2312 : BIG5); break;
                    default:
                        assert(!"Wrong encode!");
                }
            }
        } while (r <= 0);
        //read or write

        if (FD_ISSET(mConFd, &set)) {
            n = read(mConFd, buf, BUFSIZE);
            if (n > 0)
                for (int i = 0; i < n; i++)
                    mpInputManager->ProcessKey(buf[i]);
        }

        if (FD_ISSET(mTtyFd, &set)) {
            n = read(mTtyFd, buf, BUFSIZE);
            if (n > 0) {
                mpCon->Write(buf, n);
            }
        }
    } //while
}

//fork a new pty to run user's shell
//return true if successfully fork
void Zhcon::ForkPty() {
    char name[50];
    mTtyPid = forkpty(&mTtyFd, name, NULL, NULL);
    if (mTtyPid == -1)
        throw runtime_error("forkpty fail!");

    ioctl(0, TIOCSCTTY, 0);
    if (mTtyPid == 0) {
        //child
        struct passwd *userpd;

        if ((userpd = getpwuid(getuid())) == NULL)
            throw runtime_error("can not get user's shell!");

        /* close all opened file */
#ifndef OPEN_MAX
    #warning OPEN_MAX undefined so far,try define it to 64
    #define OPEN_MAX 64
#endif
        for (int i = 3; i < OPEN_MAX; i++)
            close(i);
        setsid();
        seteuid(getuid());   /* for security */
        execlp(userpd->pw_shell, userpd->pw_shell, (char *) 0);
        exit(-1);
    }
}

void Zhcon::InstallSignal() {
    struct sigaction act;
    act.sa_handler = SignalHandle;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGHUP, &act, NULL);
    sigaction(SIGINT, &act, NULL);
    sigaction(SIGQUIT, &act, NULL);
    sigaction(SIGILL, &act, NULL);
    sigaction(SIGABRT, &act, NULL);
    sigaction(SIGIOT, &act, NULL);
    sigaction(SIGBUS, &act, NULL);
    sigaction(SIGFPE, &act, NULL);
    sigaction(SIGTERM, &act, NULL);

    act.sa_handler = &SignalChild;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, NULL);

    //    act.sa_handler = &SignalAlarm;
    //    sigemptyset(&act.sa_mask);
    //    act.sa_flags = 0;
    //    sigaction(SIGALRM, &act, NULL);
}

//do some clean up before quit
void Zhcon::CleanUp() {
    struct sigaction act;
    /* done in procress and block all serious signal to prevent interrupt */
    act.sa_handler = SIG_IGN;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGHUP, &act, NULL);
    sigaction(SIGINT, &act, NULL);
    sigaction(SIGQUIT, &act, NULL);
    sigaction(SIGILL, &act, NULL);
    sigaction(SIGABRT, &act, NULL);
    sigaction(SIGIOT, &act, NULL);
    sigaction(SIGBUS, &act, NULL);
    sigaction(SIGFPE, &act, NULL);

    struct vt_mode vtm;
    signal(SIGUSR1, SIG_DFL);
    signal(SIGUSR2, SIG_DFL);
    vtm.mode = VT_AUTO;
    vtm.waitv = 0;
    vtm.relsig = 0;
    vtm.acqsig = 0;
    ioctl(0, VT_SETMODE, &vtm);
#if defined(__FreeBSD__)
    ioctl(0, VT_RELDISP, 1);
#endif

    GraphDev::Close();
    delete gpAscFont;
    delete gpHzFont;
    delete gpDecoder;
    delete mpCon;
    delete mpInputManager;
    //need restore signal ?
    tcsetattr(mConFd, TCSAFLUSH, &mOldTermios);
    ioctl(mConFd, TIOCSWINSZ, &mOldWinSize);
    ioctl(mConFd, KDSETMODE, KD_TEXT);
    setenv("LC_ALL", mOldLocale.c_str(), 1);

}

void Zhcon::InitTty() {
    // Using throw cause core dump when call destruct
    if (!isatty(fileno(stdout))) {
        printf("This is an interactive api, don't redirect stdout.\r\n");
        exit(1);
    }

    char *TtyName = ttyname(fileno(stdout));
    if (!TtyName) {
        printf("Can not get current tty name.\r\n");
        exit(1);
    }
    
    if (!(strncmp("/dev/tty", TtyName, 8) == 0 ||
        strncmp("/dev/vc/", TtyName, 8) == 0)) {
        printf("%s is not real tty or vc. Please exit current tty and try again.\r\n", TtyName);
        exit(1);
    }
    
    mConFd = open(TtyName, O_RDWR);
    if (mConFd == -1)
        throw runtime_error("Can not open console!");

    if (tcgetattr(mConFd, &mOldTermios) < 0)
        throw runtime_error("Can't get console termios.");

    struct termios t;
    t = mOldTermios;
    t.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG | NOFLSH);
    t.c_iflag &= ~(BRKINT | ICRNL | ISTRIP | IXON);
    t.c_cflag &= ~(CSIZE | PARENB);
    t.c_cflag |= CS8;
    t.c_oflag &= ~(OPOST);
    t.c_cc[VMIN] = 1;
    t.c_cc[VTIME] = 0;
#if defined(linux)
    t.c_line = 0;
#elif defined(__FreeBSD__)
    t.c_cc[VDISCARD] = _POSIX_VDISABLE;
    t.c_cc[VLNEXT] = _POSIX_VDISABLE;
    t.c_cc[VSTART] = _POSIX_VDISABLE;
    t.c_cc[VSTOP] = _POSIX_VDISABLE;
    t.c_cc[VINTR] = _POSIX_VDISABLE;
    t.c_cc[VSUSP] = _POSIX_VDISABLE;
    t.c_cc[VDSUSP] = _POSIX_VDISABLE;
    t.c_cc[VQUIT] = _POSIX_VDISABLE;
#endif
    if (tcsetattr(mConFd, TCSAFLUSH, &t) < 0)
        throw runtime_error("Can't set console termios\n");
    ioctl(0, TIOCGWINSZ, &mOldWinSize);
}

void Zhcon::SetEncode(Encode e, Encode font) {
    if (mEncode == e && mFontEncode == font)
        return;
    try {
        BaseFont *f = NULL;
        HzDecoder *h = NULL;
        string locale;
        switch (e) {
            case GB2312:
                assert(font == GB2312 || font == BIG5);
                if (font == BIG5) {
                    f = new BaseFont(mBIG5Font, 16, 16);
                    h = new GB2BIG5Decoder();
                } else {
                    f = new BaseFont(mGB2312Font, 16, 16);
                    h = new GBDecoder();
                }
                locale = "zh_CN.GB2312";
                break;
            case GBK:
                assert(font == GBK);
                f = new BaseFont(mGBKFont, 16, 16);
                h = new GBKDecoder();
                locale = "zh_CN.GBK";
                break;
            case BIG5:
                assert(font == GB2312 || font == BIG5);
                if (font == GB2312) {
                    f = new BaseFont(mGB2312Font, 16, 16);
                    h = new BIG52GBDecoder();
                } else {
                    f = new BaseFont(mBIG5Font, 16, 16);
                    h = new BIG5Decoder();
                }
                locale = "zh_TW.Big5";
                break;
            case JIS:
                assert(font == e);
                f = new BaseFont(mJISFont, 16, 16);
                h = new JISDecoder();
                locale = "ja.JIS";
                break;
            case KSCM:
                assert(font == e);
                f = new BaseFont(mKSCMFont, 16, 16);
                h = new KSCMDecoder();
                locale = "ko";
                break;
            default:
                assert(!"unknown encode and font type");
        }
        delete gpHzFont;
        gpHzFont = f;
        gpScreen->SetDblFont(gpHzFont);
        delete gpDecoder;
        gpDecoder = h;
        mEncode = e;
        mFontEncode = font;
        //i18n locale support
        setlocale(LC_ALL,locale.c_str());
        textdomain(PACKAGE);
        //Fix me:how to change child's shell env?
        if (mpCon) {
            mpCon->ResetFlagAll();
            mpCon->Redraw(true);
        }
        if (gpInputManager)
            gpInputManager->Redraw();
        Beep();
    }//try
    catch (runtime_error & e) {}
}

/*setup vt switch handle */
void Zhcon::InstallVtHandle() {
    vt_mode vtm;
    if (ioctl(mConFd, VT_GETMODE, &vtm))
        throw runtime_error("ioctl VT_GETMODE failed!");

    vtm.mode = VT_PROCESS;
    vtm.relsig = SIGUSR1;
    vtm.acqsig = SIGUSR2;
#if defined(__FreeBSD__)
    // based on cce
    vtm.frsig = SIGUSR1;  //added by raner or ioctl will fail
#endif
    signal(SIGUSR1, ::SignalVtLeave);
    signal(SIGUSR2, ::SignalVtEnter);
    if (ioctl(mConFd, VT_SETMODE, &vtm))
        throw runtime_error("ioctl VT_SETMODE failed!");
}

void Zhcon::SignalVtLeave() {
    mpCon->CursorHide();  // when window has cursor, put in hide()
    mpCon->Hide();
    mpInputManager->Hide();
    signal(SIGUSR1, ::SignalVtLeave);
    // not needed, may change color platte when libggi used
    // TextMode();
    gpScreen->SwitchToText();
    ioctl(mConFd, VT_RELDISP, 1);
}

void Zhcon::SignalVtEnter() {
    signal(SIGUSR2, ::SignalVtEnter);
    // GraphMode();
    gpScreen->SwitchToGraph();
    gpScreen->FillRect(0, 0, gpScreen->Width() - 1, gpScreen->Height() - 1,
                       0);
    mpCon->Show();
    mpCon->CursorShow();  // when window has cursor, put in show()
    mpInputManager->Show();
    ioctl(mConFd, VT_RELDISP, VT_ACKACQ);
}

void Zhcon::GraphMode() {
    if (ioctl(mConFd, KDSETMODE, KD_GRAPHICS))
        throw runtime_error("ioctl KDSETMODE failed!");
    ioctl(mConFd, TIOCCONS, NULL);
}

void Zhcon::TextMode() {
    if (ioctl(mConFd, KDSETMODE, KD_TEXT))
        throw runtime_error("ioctl KDSETMODE failed!");
    ioctl(mConFd, TIOCCONS, NULL);
}

//set encode,locale then load appropriate fonts
void Zhcon::InitLocale(ConfigFile& f){
    bindtextdomain(PACKAGE,NULL);
    if (getenv("LC_ALL"))
        mOldLocale = getenv("LC_ALL");

    string prefix = PREFIX"/lib/zhcon/";
    mASCIIFont = prefix + f.GetOption(string("ascfont"), string(ASCIIFONT));
    mGB2312Font = prefix + f.GetOption(string("gbfont"), string(GB2312FONT));
    mGBKFont = prefix + f.GetOption(string("gbkfont"), string(GBKFONT));
    mBIG5Font = prefix + f.GetOption(string("big5font"), string(BIG5FONT));
    mJISFont = prefix + f.GetOption(string("jisfont"), string(JISFONT));
    mKSCMFont = prefix + f.GetOption(string("kscmfont"), string(KSCMFONT));

    gpAscFont = new BaseFont(mASCIIFont, 8, 16);
    gpScreen->SetAscFont(gpAscFont);
    //dblfont loaded in SetEncode()

    string s;
    s = f.GetOption(string("defaultencode"), string("gb2312"));
    if (s == "gb2312") {
        SetEncode(GB2312,GB2312);
        setenv("LC_ALL", "zh_CN.GB2312", 1);
        mDefaultEncode = GB2312;
    } else if (s == "gbk") {
        SetEncode(GBK,GBK);
        setenv("LC_ALL", "zh_CN.GBK", 1);
        mDefaultEncode = GBK;
    } else if (s == "big5") {
        SetEncode(BIG5,BIG5);
        setenv("LC_ALL", "zh_TW.Big5", 1);
        mDefaultEncode = BIG5;
    } else if (s == "jis") {
        SetEncode(JIS,JIS);
        setenv("LC_ALL", "ja.JIS", 1);
        mDefaultEncode = JIS;
    } else if (s == "kscm") {
        SetEncode(KSCM,KSCM);
        setenv("LC_ALL", "ko", 1);
        mDefaultEncode = GBK;
    } else {
        throw runtime_error("unable to set default encode!");
    }

    s = f.GetOption(string("autoencode"), string("manual"));
    if (s == "auto")
        mAutoEncode = AUTO;
    else if (s == "auto-gb")
        mAutoEncode = AUTO_GB;
    else if (s == "auto-big5")
        mAutoEncode = AUTO_BIG5;
    else
        mAutoEncode = MANUAL;
}

void Zhcon::InitGraphDev(ConfigFile& f){
    bool r;
#if defined(linux)
    r = GraphDev::Open();
#elif defined(__FreeBSD__)
    int xres = f.GetOption(string("x_resolution"), 640);
    int yres = f.GetOption(string("y_resolution"), 480);
    int depth = f.GetOption(string("color_depth"), 4);
    r = GraphDev::Open(xres, yres, depth);
#endif
    if (!r)
        throw(runtime_error("Can not open graph device,make sure "
            "you have a kernel compiled with framebuffer support or "
            "you have a vga card."));
    gpScreen = GraphDev::mpGraphDev;
    GraphDev::mBlankLineHeight = f.GetOption(string("blanklineheight"), 0);
    if (GraphDev::mBlankLineHeight < 0)
        GraphDev::mBlankLineHeight = 0;
    gpScreen->FillRect(0, 0, gpScreen->Width() - 1, gpScreen->Height() - 1,0);
}

void Zhcon::InitCon(ConfigFile& f){
    /*
    int BarHeight;
    if (gpScreen->BlockHeight()/2 < 4)
        BarHeight = gpScreen->BlockHeight() + 4;
    else
        BarHeight = gpScreen->BlockHeight() + gpScreen->BlockHeight()/2;
    mpCon = new Console(0, 0, gpScreen->Width() - 1,
                        gpScreen->Height() - BarHeight - 1);
    */
    mpCon = new Console(0, 0, gpScreen->Width() - 1,
                        gpScreen->Height() - 1);
    int CursorType;
    CursorType = f.GetOption(string("cursortype"), CUR_DEF);
    mpCon->SetCursorType(CursorType);
    gpConsole = mpCon;
}

void Zhcon::InitInputManager(ConfigFile& f){
    string s;
    s = f.GetOption(string("zhconpath"), string(PREFIX"/lib/zhcon/"));
    NativeInputServer::SetDataPath(s);
#ifdef HAVE_UNICON_LIB
    s = f.GetOption(string("uniconpath"), string("/usr/lib/unicon/"));
    UniconInputServer::SetDataPath(s);
#endif
    string sOverSpot, sNativeBar;
    sOverSpot = f.GetOption(string("overspotcolor"), string("0,7,1,1,15,8"));
    sNativeBar = f.GetOption(string("nativebarcolor"), string("15,4,11,14,0,12"));
    s = f.GetOption(string("inputstyle"),string("overspot"));
    if (s == "overspot")
        mpInputManager = new InputManager(this,mpCon,InputManager::OverSpot, sOverSpot, sNativeBar);
    else if (s == "nativebar")
        mpInputManager = new InputManager(this,mpCon,InputManager::NativeBar, sOverSpot, sNativeBar);
    else
        throw runtime_error("unknown input style.");
    mpInputManager->LoadImmInfo(f);
    mpInputManager->Show();
    gpInputManager = mpInputManager;
    if (f.GetOption("promptmode",true))
        gpInputManager->PromptMode();
}

void Zhcon::InitMisc(ConfigFile& f){
    Beep(f.GetOption(string("beep"),true));
}
void Zhcon::StartupMsg(){
    #include "logo.h"
    string scr = screendata;
    string s;
    s = "Linux   ";
    scr.replace(scr.find("PLATFORM"),8,s);
    s = VERSION;
    s.resize(7,' ');
    scr.replace(scr.find("VERSION"),7,s);
    strstream buf;
    buf<<_("screen resolution")<<": "<<gpScreen->Width()<<"X"<<
        gpScreen->Height()<<" ["<<gpConsole->MaxCols()<<","<<
        gpConsole->MaxRows()<<"]"<<"\r\n";
    buf<<_("default encode")<<": "<<GetEncode()<<"\r\n";
    scr += buf.str();
    gpConsole->Write(scr.c_str(),scr.size());
}

string Zhcon::GetEncode(){
    string s;
    switch (mAutoEncode) {
        case Zhcon::AUTO:
            s = "A:";
            break;
        case Zhcon::AUTO_GB:
            s = "A-GB:";
            break;
        case Zhcon::AUTO_BIG5:
            s = "A-B5:";
            break;
        case Zhcon::MANUAL:
            s = "M:";
            break;
    }
    switch (mEncode) {
        case ASCII:
            s += "ASCII";
            break;
        case GB2312:
                s += "GB2312";
            break;
        case GBK:
            s += "GBK";
            break;
        case BIG5:
                s += "BIG5";
            break;
        case JIS:
            s += "JIS";
            break;
        case KSCM:
            s += "KSCM";
            break;
        default:
            s += "Unknown";
    }
    return s;
}
//return a random tip string
string Zhcon::GetATip(){
    static unsigned i = 0;
    if (!mShowTips) return "";
    if (++i >= (sizeof(mpTips)/sizeof(char*)) )
        i = 0;
    return string(_(mpTips[i]));
}
