/*
 * termios_dumper.c
 *
 * genarate a list of current states for acitve ttys.
 *
 * Copyright (C) 2008, 2009 Pylone, Inc.
 * Author: MINAMI Hirokazu <minami@pylone.jp>
 *
 * This software is licensed under the terms of the GNU General Public
 * License version 2, as published by the Free Software Foundation, and
 * may be copied, distributed, and modified under those terms.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>

#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/termios.h>


/* interprit termios bits */
#define TERMOIS_BIT(NAME) {#NAME, NAME, NULL}
#define TERMOIS_MAP(NAME, ...)                              \
    {#NAME, NAME, (struct termios_masks []) __VA_ARGS__}

struct termios_masks{
    const char *name;
    unsigned int mask;
    const struct termios_masks *submap;
};

static const struct termios_masks iflags[] =  {
    TERMOIS_BIT(IGNBRK),
    TERMOIS_BIT(BRKINT),
    TERMOIS_BIT(IGNPAR),
    TERMOIS_BIT(PARMRK),
    TERMOIS_BIT(INPCK),
    TERMOIS_BIT(ISTRIP),
    TERMOIS_BIT(INLCR),
    TERMOIS_BIT(IGNCR),
    TERMOIS_BIT(ICRNL),
    TERMOIS_BIT(IUCLC),
    TERMOIS_BIT(IXON),
    TERMOIS_BIT(IXANY),
    TERMOIS_BIT(IXOFF),
    TERMOIS_BIT(IMAXBEL),
    TERMOIS_BIT(IUTF8),
    {NULL}
};
static const struct termios_masks oflags[] =  {
    TERMOIS_BIT(OPOST),
    TERMOIS_BIT(OLCUC),
    TERMOIS_BIT(ONLCR),
    TERMOIS_BIT(OCRNL),
    TERMOIS_BIT(ONOCR),
    TERMOIS_BIT(ONLRET),
    TERMOIS_BIT(OFILL),
    TERMOIS_BIT(OFDEL),
    TERMOIS_MAP(NLDLY,{
            TERMOIS_BIT(      NL0),
                TERMOIS_BIT(  NL1),
                {NULL}}),
    TERMOIS_MAP(CRDLY,{
            TERMOIS_BIT(      CR0),
                TERMOIS_BIT(  CR1),
                TERMOIS_BIT(  CR2),
                TERMOIS_BIT(  CR3),
                {NULL}}),
    TERMOIS_MAP(TABDLY,{
            TERMOIS_BIT(      TAB0),
                TERMOIS_BIT(  TAB1),
                TERMOIS_BIT(  TAB2),
                TERMOIS_BIT(  TAB3),
                TERMOIS_BIT(  XTABS),
                {NULL}}),
    TERMOIS_MAP(BSDLY,{
            TERMOIS_BIT(      BS0),
                TERMOIS_BIT(  BS1),
                {NULL}}),
    TERMOIS_MAP(VTDLY,{
            TERMOIS_BIT(      VT0),
                TERMOIS_BIT(  VT1),
                {NULL}}),
    TERMOIS_MAP(FFDLY,{
            TERMOIS_BIT(      FF0),
                TERMOIS_BIT(  FF1),
                {NULL}}),
    {NULL}
};
static const struct termios_masks cflags[] =  {
    TERMOIS_MAP(CBAUD,{
            TERMOIS_BIT( B0),
                TERMOIS_BIT( B50),
                TERMOIS_BIT( B75),
                TERMOIS_BIT( B110),
                TERMOIS_BIT( B134),
                TERMOIS_BIT( B150),
                TERMOIS_BIT( B200),
                TERMOIS_BIT( B300),
                TERMOIS_BIT( B600),
                TERMOIS_BIT( B1200),
                TERMOIS_BIT( B1800),
                TERMOIS_BIT( B2400),
                TERMOIS_BIT( B4800),
                TERMOIS_BIT( B9600),
                TERMOIS_BIT( B19200),
                TERMOIS_BIT( B38400),
                {NULL}}),
    TERMOIS_MAP(CSIZE,{
            TERMOIS_BIT(  CS5),
                TERMOIS_BIT(  CS6),
                TERMOIS_BIT(  CS7),
                TERMOIS_BIT(  CS8),
                {NULL}}),
    TERMOIS_BIT(CSTOPB),
    TERMOIS_BIT(CREAD),
    TERMOIS_BIT(PARENB),
    TERMOIS_BIT(PARODD),
    TERMOIS_BIT(HUPCL),
    TERMOIS_BIT(CLOCAL),
    TERMOIS_MAP(CBAUDEX,{
            TERMOIS_BIT(   BOTHER),
                TERMOIS_BIT(   B57600),
                TERMOIS_BIT(  B115200),
                TERMOIS_BIT(  B230400),
                TERMOIS_BIT(  B460800),
                TERMOIS_BIT(  B500000),
                TERMOIS_BIT(  B576000),
                TERMOIS_BIT(  B921600),
                TERMOIS_BIT( B1000000),
                TERMOIS_BIT( B1152000),
                TERMOIS_BIT( B1500000),
                TERMOIS_BIT( B2000000),
                TERMOIS_BIT( B2500000),
                TERMOIS_BIT( B3000000),
                TERMOIS_BIT( B3500000),
                TERMOIS_BIT( B4000000),
                {NULL}}),
    TERMOIS_BIT(CIBAUD),
    TERMOIS_BIT(CMSPAR),
    TERMOIS_BIT(CRTSCTS),
    {NULL}
};

static const struct termios_masks lflags[] =  {
    TERMOIS_BIT(ISIG),
    TERMOIS_BIT(ICANON),
    TERMOIS_BIT(XCASE),
    TERMOIS_BIT(ECHO),
    TERMOIS_BIT(ECHOE),
    TERMOIS_BIT(ECHOK),
    TERMOIS_BIT(ECHONL),
    TERMOIS_BIT(NOFLSH),
    TERMOIS_BIT(TOSTOP),
    TERMOIS_BIT(ECHOCTL),
    TERMOIS_BIT(ECHOPRT),
    TERMOIS_BIT(ECHOKE),
    TERMOIS_BIT(FLUSHO),
    TERMOIS_BIT(PENDIN),
    TERMOIS_BIT(IEXTEN),
    {NULL}
};

static const struct termios_masks *template[] = {
    iflags,
    oflags,
    cflags,
    lflags,
};


/* refer 'tty_driver' in tty core which is not exported. */
static struct list_head *tty_drivers_head;

/* try to locate the true head of list by its magic number.
 * NOTE: may fail if a memory block pointed by list_entry()
 *  accidentaly contains TTY_DRIVER_MAGIC.
 * (i.e. may fail without valid reson in 1 of 2^32 times)
 */
static int register_tty_drivers_head()
{
    int ret = 0;

    mutex_lock(&tty_mutex);
    do{/* traverse list with tty_mutex */
        struct tty_struct *tty;
        struct tty_driver *driver;
        if(! (tty = get_current_tty()) ){
            ret = -ENODEV;
            break;
        }
        list_for_each_entry(driver, &tty->driver->tty_drivers, tty_drivers) {
            if(driver->magic != TTY_DRIVER_MAGIC){
                tty_drivers_head = &driver->tty_drivers;
                break;
            }
        }
    }while(0);
    mutex_unlock(&tty_mutex);

    if( !tty_drivers_head ){
        printk(KERN_INFO "%s:failed to discover tty_drivers\n",
               __func__);
        ret = -ENODEV;
    }

    return ret;
}

/* check whether 'tty_index'th tty of the tty driver is active */
static int is_valid_tty(struct tty_driver *driver, int tty_index)
{
    if(driver
       && (driver->magic == TTY_DRIVER_MAGIC)
       && (driver->ttys)
       && (tty_index >= 0)
       && (driver->num > tty_index)
       && (driver->ttys[tty_index]) ){
        return 1;
    }
    return 0;
}

/* seq_file iterator */
struct seq_iter{
    struct tty_driver *driver;
    int driver_index;

    struct tty_struct *tty;
    int tty_index;

    enum {
        TERMIO_I = 0,
        TERMIO_O = 1,
        TERMIO_C = 2,
        TERMIO_L = 3,
    } mask_type;

    int pos;
    int pos_offset;
};

/* switch the tty pointed by iterator to new one */
static int get_next_tty(struct seq_iter *iter)
{
    int i = 0;
    struct tty_driver *driver;

    if( is_valid_tty(iter->driver, iter->tty_index+1) ){
        iter->tty_index++;
        iter->tty = iter->driver->ttys[iter->tty_index];
        return 0;
    }

    /* try to get next device from another driver */
    list_for_each_entry(driver, tty_drivers_head, tty_drivers) {
        if( (iter->driver_index < i)
           && is_valid_tty(driver, 0) ){
            iter->driver = driver;
            iter->driver_index = i;
            iter->tty = driver->ttys[0];
            iter->tty_index = 0;
            return 0;
        }
        i++;
    }
    /* no more tty driver */
    return -1;
}

/* advance iterator till (*pos)th element. */
static void *update_iterator(struct seq_iter *iter, loff_t *pos)
{
    int to_walk;

    if(!iter->tty){
        /* called for the fisrst time */
        if(get_next_tty(iter) < 0){
            return NULL;
        }
        iter->mask_type = TERMIO_I;
        iter->pos = 0;
        iter->pos_offset = 0;
    }

rescan:
    to_walk = (*pos) - iter->pos - iter->pos_offset;

    if(to_walk < 0){
        printk(KERN_INFO "** %s : invalid location %d\n",
               __func__, (int)(*pos));
        return NULL;
    }

    while(to_walk){
        iter->pos++;
        if(!template[iter->mask_type][iter->pos].name){
            iter->pos_offset += iter->pos;
            iter->pos = 0;
            if(iter->mask_type >= TERMIO_L){
                break;
            }
            iter->mask_type++;
        }
        to_walk--;
    }

    if(to_walk > 0){
        if(get_next_tty(iter) < 0){
            return NULL;
        }
        iter->mask_type = TERMIO_I;
        goto rescan;
    }
    return iter;
}

static void *s_next(struct seq_file *m, void *p, loff_t *pos)
{
    ++(*pos);
    if(!(update_iterator(p, pos))){
        kfree(p);
        return NULL;
    }
    return p;
}

static void *s_start(struct seq_file *m, loff_t *pos)
{
    struct seq_iter *iter;

/*    mutex_lock(&tty_mutex); */

    if( !(iter = kmalloc(sizeof(*iter), GFP_KERNEL)) )
        return NULL;

    iter->driver = NULL;
    iter->driver_index = -1;

    iter->tty = NULL;
    iter->tty_index = 0;

    iter->mask_type = TERMIO_I;
    iter->pos = 0;

    iter->pos_offset = 0;

    if(!(update_iterator(iter, pos))){
        kfree(iter);
        return NULL;
    }
    return iter;
}

static void s_stop(struct seq_file *m, void *p)
{
    struct seq_iter *iter = p;

    if(iter){
        kfree(iter);
    }
/*    mutex_unlock(&tty_mutex); */
}

static int s_show(struct seq_file *m, void *p)
{
    unsigned int state;
    const struct termios_masks *cur;
    struct seq_iter *iter = p;
    char ftype = '?';
    const char *readable_value;

    if(!iter || !iter->tty){
        return SEQ_SKIP;
    }

    cur = template[iter->mask_type] + iter->pos;

    switch(iter->mask_type){
    case TERMIO_I:
        ftype = 'I';
        state = (iter->tty)->termios->c_iflag & (cur->mask);
        break;
    case TERMIO_O:
        ftype = 'O';
        state = (iter->tty)->termios->c_oflag & (cur->mask);
        break;
    case TERMIO_C:
        ftype = 'C';
        state = (iter->tty)->termios->c_cflag & (cur->mask);
        break;
    case TERMIO_L:
        ftype = 'L';
        state = (iter->tty)->termios->c_lflag & (cur->mask);
        break;
    default:
        return -EINVAL;
    }

    if(cur->submap){
        int i = 0;
        readable_value = "???";
        while(cur->submap[i].name){
            if(cur->submap[i].mask == state){
                readable_value = cur->submap[i].name;
                break;
            }
            i++;
        }
    }else{
        readable_value = (state) ? "On":"Off";
    }
    return seq_printf(m, "%s:%s:%c:%-8s:%-8s[%08x]\n",
                      iter->driver->name, iter->tty->name, ftype,
                      cur->name, readable_value, state);
}

static const struct seq_operations seqfile_ops = {
    .start = s_start,
    .next  = s_next,
    .stop  = s_stop,
    .show  = s_show
};

static int termios_dumper_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &seqfile_ops);
}


/************************************************************/
static struct dentry *fs_root;
static struct dentry *fs_file;

static const struct file_operations fops = {
    .open    = termios_dumper_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = seq_release,
};

static int __init termios_dumper_init(void)
{
    if(IS_ERR(fs_root = debugfs_create_dir(KBUILD_BASENAME, NULL)))
        return PTR_ERR(fs_root);

    if(IS_ERR(fs_file = debugfs_create_file("state", 0444, fs_root, NULL,
                                            &fops)))
        return PTR_ERR(fs_file);

    if(register_tty_drivers_head() < 0)
        return -ENODEV;

    printk(KERN_INFO "*** %s\n", __func__);

    return 0;
}

static void __exit termios_dumper_exit(void){
    printk(KERN_INFO "*** %s\n", __func__);
    debugfs_remove(fs_file);
    debugfs_remove(fs_root);
}

module_init(termios_dumper_init)
module_exit(termios_dumper_exit)
MODULE_LICENSE("GPL");
