/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "selector"

#include <assert.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#include <cutils/array.h>
#include <cutils/selector.h>

#include "loghack.h"

struct Selector {
    Array* selectableFds;
    bool looping;
    fd_set readFds;
    fd_set writeFds;
    fd_set exceptFds;
    int maxFd;
    int wakeupPipe[2];
    SelectableFd* wakeupFd;

    bool inSelect;
    pthread_mutex_t inSelectLock; 
};

/** Reads and ignores wake up data. */ 
static void eatWakeupData(SelectableFd* wakeupFd) {
    static char garbage[64];
    if (read(wakeupFd->fd, garbage, sizeof(garbage)) < 0) {
        if (errno == EINTR) {
            ALOGI("read() interrupted.");    
        } else {
            LOG_ALWAYS_FATAL("This should never happen: %s", strerror(errno));
        }
    }
}

static void setInSelect(Selector* selector, bool inSelect) {
    pthread_mutex_lock(&selector->inSelectLock);
    selector->inSelect = inSelect;
    pthread_mutex_unlock(&selector->inSelectLock);
}

static bool isInSelect(Selector* selector) {
    pthread_mutex_lock(&selector->inSelectLock);
    bool inSelect = selector->inSelect;
    pthread_mutex_unlock(&selector->inSelectLock);
    return inSelect;
}

void selectorWakeUp(Selector* selector) {
    if (!isInSelect(selector)) {
        // We only need to write wake-up data if we're blocked in select().
        return;
    }
    
    static char garbage[1];
    if (write(selector->wakeupPipe[1], garbage, sizeof(garbage)) < 0) {
        if (errno == EINTR) {
            ALOGI("read() interrupted.");    
        } else {
            LOG_ALWAYS_FATAL("This should never happen: %s", strerror(errno));
        }
    }
}

Selector* selectorCreate(void) {
    Selector* selector = calloc(1, sizeof(Selector));
    if (selector == NULL) {
        LOG_ALWAYS_FATAL("malloc() error.");
    }
    selector->selectableFds = arrayCreate();
    
    // Set up wake-up pipe.
    if (pipe(selector->wakeupPipe) < 0) {
        LOG_ALWAYS_FATAL("pipe() error: %s", strerror(errno));
    }
    
    ALOGD("Wakeup fd: %d", selector->wakeupPipe[0]);
    
    SelectableFd* wakeupFd = selectorAdd(selector, selector->wakeupPipe[0]);
    if (wakeupFd == NULL) {
        LOG_ALWAYS_FATAL("malloc() error.");
    }
    wakeupFd->onReadable = &eatWakeupData; 
    
    pthread_mutex_init(&selector->inSelectLock, NULL);

    return selector;
}

SelectableFd* selectorAdd(Selector* selector, int fd) {
    assert(selector != NULL);

    SelectableFd* selectableFd = calloc(1, sizeof(SelectableFd));
    if (selectableFd != NULL) {
        selectableFd->selector = selector;
        selectableFd->fd = fd;
    
        arrayAdd(selector->selectableFds, selectableFd);
    }

    return selectableFd;
}

/**
 * Adds an fd to the given set if the callback is non-null. Returns true
 * if the fd was added.
 */
static inline bool maybeAdd(SelectableFd* selectableFd,
        void (*callback)(SelectableFd*), fd_set* fdSet) {
    if (callback != NULL) {
        FD_SET(selectableFd->fd, fdSet);
        return true;
    }
    return false;
}

/**
 * Removes stale file descriptors and initializes file descriptor sets.
 */
static void prepareForSelect(Selector* selector) {
    fd_set* exceptFds = &selector->exceptFds;
    fd_set* readFds = &selector->readFds;
    fd_set* writeFds = &selector->writeFds;
    
    FD_ZERO(exceptFds);
    FD_ZERO(readFds);
    FD_ZERO(writeFds);

    Array* selectableFds = selector->selectableFds;
    int i = 0;
    selector->maxFd = 0;
    int size = arraySize(selectableFds);
    while (i < size) {
        SelectableFd* selectableFd = arrayGet(selectableFds, i);
        if (selectableFd->remove) {
            // This descriptor should be removed.
            arrayRemove(selectableFds, i);
            size--;
            if (selectableFd->onRemove != NULL) {
                selectableFd->onRemove(selectableFd);
            }
            free(selectableFd);
        } else {
            if (selectableFd->beforeSelect != NULL) {
                selectableFd->beforeSelect(selectableFd);
            }
            
            bool inSet = false;
            if (maybeAdd(selectableFd, selectableFd->onExcept, exceptFds)) {
                ALOGD("Selecting fd %d for writing...", selectableFd->fd);
                inSet = true;
            }
            if (maybeAdd(selectableFd, selectableFd->onReadable, readFds)) {
                ALOGD("Selecting fd %d for reading...", selectableFd->fd);
                inSet = true;
            }
            if (maybeAdd(selectableFd, selectableFd->onWritable, writeFds)) {
                inSet = true;
            }

            if (inSet) {
                // If the fd is in a set, check it against max.
                int fd = selectableFd->fd;
                if (fd > selector->maxFd) {
                    selector->maxFd = fd;
                }
            }
            
            // Move to next descriptor.
            i++;
        }
    }
}

/**
 * Invokes a callback if the callback is non-null and the fd is in the given
 * set.
 */
static inline void maybeInvoke(SelectableFd* selectableFd,
        void (*callback)(SelectableFd*), fd_set* fdSet) {
    if (callback != NULL && !selectableFd->remove && 
            FD_ISSET(selectableFd->fd, fdSet)) {
        ALOGD("Selected fd %d.", selectableFd->fd);
        callback(selectableFd);
    }
}

/**
 * Notifies user if file descriptors are readable or writable, or if
 * out-of-band data is present.
 */
static void fireEvents(Selector* selector) {
    Array* selectableFds = selector->selectableFds;
    int size = arraySize(selectableFds);
    int i;
    for (i = 0; i < size; i++) {
        SelectableFd* selectableFd = arrayGet(selectableFds, i);
        maybeInvoke(selectableFd, selectableFd->onExcept,
                &selector->exceptFds);
        maybeInvoke(selectableFd, selectableFd->onReadable,
                &selector->readFds);
        maybeInvoke(selectableFd, selectableFd->onWritable,
                &selector->writeFds);
    }
}

void selectorLoop(Selector* selector) {
    // Make sure we're not already looping.
    if (selector->looping) {
        LOG_ALWAYS_FATAL("Already looping.");
    }
    selector->looping = true;
    
    while (true) {
        setInSelect(selector, true);
        
        prepareForSelect(selector);

        ALOGD("Entering select().");
        
        // Select file descriptors.
        int result = select(selector->maxFd + 1, &selector->readFds, 
                &selector->writeFds, &selector->exceptFds, NULL);
        
        ALOGD("Exiting select().");
        
        setInSelect(selector, false);
        
        if (result == -1) {
            // Abort on everything except EINTR.
            if (errno == EINTR) {
                ALOGI("select() interrupted.");    
            } else {
                LOG_ALWAYS_FATAL("select() error: %s", 
                        strerror(errno));
            }
        } else if (result > 0) {
            fireEvents(selector);
        }
    }
}
